diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 809bb97e..111a813a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 --- -**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. +**补充说明** +与问题相关的其它说明 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/deployment-error.md b/.github/ISSUE_TEMPLATE/deployment-error.md index 12e47613..14c6bc8a 100644 --- a/.github/ISSUE_TEMPLATE/deployment-error.md +++ b/.github/ISSUE_TEMPLATE/deployment-error.md @@ -1,7 +1,28 @@ --- name: Deployment error (部署错误) -about: Do you need help to install NotionNext? +about: 在安装部署NotionNext时需要什么帮助吗 title: '' labels: deployment assignees: tangly1024 --- + + + + +**描述遇到的问题** +简单说明你遇到的问题,相关的日志、错误信息 + +**相应配置** +相关的配置,例如notion_page_id;你的网站地址 + +**截图** +相关的页面,应该用结果 + +**环境** + +- 操作系统: [例如. iOS, Android, macOS, windows] +- 浏览器 [例如. chrome, safari, firefox] +- 版本 [e.g. 22] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e454490b..8a50c9d3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,18 +7,18 @@ assignees: tangly1024 --- -**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. +**补充说明** +补充与此特性相关的内容 diff --git a/blog.config.js b/blog.config.js index 19720ff9..7b94df71 100644 --- a/blog.config.js +++ b/blog.config.js @@ -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 diff --git a/components/Fireworks.js b/components/Fireworks.js new file mode 100644 index 00000000..3ccdec7e --- /dev/null +++ b/components/Fireworks.js @@ -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 +} + +/** + * 创建烟花 + * @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 + ) +} diff --git a/components/Live2D.js b/components/Live2D.js index 89e68301..e91a3806 100644 --- a/components/Live2D.js +++ b/components/Live2D.js @@ -23,15 +23,20 @@ export default function Live2D() { return } +/** + * 加载宠物 + */ 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) } diff --git a/components/NotionPage.js b/components/NotionPage.js index bff705f5..fd03fabc 100644 --- a/components/NotionPage.js +++ b/components/NotionPage.js @@ -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
+ 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
{
} -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 diff --git a/lib/global.js b/lib/global.js index ea93caff..6e0c5fee 100644 --- a/lib/global.js +++ b/lib/global.js @@ -49,7 +49,7 @@ export function GlobalContextProvider({ children }) { }, []) return ( - + {children} ) diff --git a/lib/notion.js b/lib/notion.js index a2f19e88..51c02180 100644 --- a/lib/notion.js +++ b/lib/notion.js @@ -1,4 +1,3 @@ export { getAllPosts } from './notion/getAllPosts' export { getAllTags } from './notion/getAllTags' export { getPostBlocks } from './notion/getPostBlocks' -export { getAllCategories } from './notion/getAllCategories' diff --git a/lib/notion/getAllCategories.js b/lib/notion/getAllCategories.js deleted file mode 100644 index b9a543d8..00000000 --- a/lib/notion/getAllCategories.js +++ /dev/null @@ -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 - } -} diff --git a/lib/notion/getAllPosts.js b/lib/notion/getAllPosts.js index fbe588da..47198789 100644 --- a/lib/notion/getAllPosts.js +++ b/lib/notion/getAllPosts.js @@ -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 -} diff --git a/lib/notion/getAllTags.js b/lib/notion/getAllTags.js index 174055d1..33e82f51 100644 --- a/lib/notion/getAllTags.js +++ b/lib/notion/getAllTags.js @@ -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 [] } diff --git a/lib/notion/getNotionData.js b/lib/notion/getNotionData.js index 55a3d031..0b2d5ba1 100644 --- a/lib/notion/getNotionData.js +++ b/lib/notion/getNotionData.js @@ -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 } } diff --git a/lib/notion/getPostBlocks.js b/lib/notion/getPostBlocks.js index 52cbdf32..1f58d94f 100644 --- a/lib/notion/getPostBlocks.js +++ b/lib/notion/getPostBlocks.js @@ -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 -} diff --git a/lib/utils.js b/lib/utils.js index be9db197..e4b854a7 100644 --- a/lib/utils.js +++ b/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)) diff --git a/package.json b/package.json index 9b48294c..99bb3f68 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/[slug].js b/pages/[slug].js index cabde864..43c98844 100644 --- a/pages/[slug].js +++ b/pages/[slug].js @@ -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 + 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 } + 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 } diff --git a/pages/_app.js b/pages/_app.js index d9ae363f..3554446b 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -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) && } + {JSON.parse(BLOG.DEBUG) && } + {BLOG.ANALYTICS_ACKEE_TRACKER && } + {BLOG.ANALYTICS_GOOGLE_ID && } + {JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && } + {BLOG.ADSENSE_GOOGLE_ID && } + {BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && } + {JSON.parse(BLOG.FIREWORKS) && } + + return ( - - {BLOG.THEME_SWITCH && } - {BLOG.DEBUG && } - {BLOG.ANALYTICS_ACKEE_TRACKER && } - {BLOG.ANALYTICS_GOOGLE_ID && } - {JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && } - {BLOG.ADSENSE_GOOGLE_ID && } - {BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && } - {/* FontawesomeCDN */} - - - + + {/* FontawesomeCDN */} + + {externalPlugins} + + ) } diff --git a/pages/archive/index.js b/pages/archive/index.js index 5241697d..e66b2abd 100644 --- a/pages/archive/index.js +++ b/pages/archive/index.js @@ -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' } diff --git a/pages/article/[slug].js b/pages/article/[slug].js index 538cbc90..40fe2ef2 100644 --- a/pages/article/[slug].js +++ b/pages/article/[slug].js @@ -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 } + 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 ( diff --git a/pages/category/index.js b/pages/category/index.js index 1545408b..f099d29f 100644 --- a/pages/category/index.js +++ b/pages/category/index.js @@ -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' } diff --git a/pages/feed.js b/pages/feed.js index ccfe5ff4..127f9402 100644 --- a/pages/feed.js +++ b/pages/feed.js @@ -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() diff --git a/pages/index.js b/pages/index.js index 9b8c8511..fe8de2cb 100644 --- a/pages/index.js +++ b/pages/index.js @@ -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' } diff --git a/pages/page/[page].js b/pages/page/[page].js index 1de94693..b6787758 100644 --- a/pages/page/[page].js +++ b/pages/page/[page].js @@ -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' } diff --git a/pages/search/index.js b/pages/search/index.js index 222d03d0..80cfe52c 100644 --- a/pages/search/index.js +++ b/pages/search/index.js @@ -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' } diff --git a/pages/tag/[tag].js b/pages/tag/[tag].js index b5f36c0a..be89d2fa 100644 --- a/pages/tag/[tag].js +++ b/pages/tag/[tag].js @@ -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 { diff --git a/pages/tag/index.js b/pages/tag/index.js index 50c9ff8c..302511dc 100644 --- a/pages/tag/index.js +++ b/pages/tag/index.js @@ -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 diff --git a/styles/globals.css b/styles/globals.css index 1df15802..feb5f75e 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -145,4 +145,12 @@ nav { .notion-code-copy-button > svg{ pointer-events:none -} \ No newline at end of file +} + +.fireworks{ + position: fixed; + left: 0; + top: 0; + z-index: 1000; + pointer-events: none; +} diff --git a/styles/notion.css b/styles/notion.css index b176aed2..76807da7 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -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 { diff --git a/themes/example/Layout404.js b/themes/example/Layout404.js index cd28a607..5f92f0cc 100644 --- a/themes/example/Layout404.js +++ b/themes/example/Layout404.js @@ -1,6 +1,7 @@ +import LayoutBase from './LayoutBase' -export const Layout404 = () => { - return
- 404 Not found. -
+export const Layout404 = (props) => { + return + 404 Not found. + } diff --git a/themes/example/LayoutArchive.js b/themes/example/LayoutArchive.js index 9edb9c24..1b3d1e34 100644 --- a/themes/example/LayoutArchive.js +++ b/themes/example/LayoutArchive.js @@ -24,15 +24,13 @@ export const LayoutArchive = props => { }) return ( -
+
{Object.keys(archivePosts).map(archiveTitle => (
-
+
{archiveTitle}
+
    {archivePosts[archiveTitle].map(post => (
  • { - 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 ( -
    - - {/* 导航菜单 */} -
    -
    -
    - - - -
    {siteInfo.title}
    -
    - -
    - -
    -
    +
    + + {/* 顶栏LOGO */} +
    - {/* 内容主体 */} -
    -
    {children}
    -
    -
    - -
    -
    -
    + {/* 菜单 */} +
    + {/* 主体 */} +
    + + + + <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> ) } diff --git a/themes/example/LayoutCategory.js b/themes/example/LayoutCategory.js index 85d399f6..ef344ea2 100644 --- a/themes/example/LayoutCategory.js +++ b/themes/example/LayoutCategory.js @@ -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 > } diff --git a/themes/example/LayoutCategoryIndex.js b/themes/example/LayoutCategoryIndex.js index b748b140..7690b316 100644 --- a/themes/example/LayoutCategoryIndex.js +++ b/themes/example/LayoutCategoryIndex.js @@ -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> diff --git a/themes/example/LayoutIndex.js b/themes/example/LayoutIndex.js index 11da972e..7d6a17a6 100644 --- a/themes/example/LayoutIndex.js +++ b/themes/example/LayoutIndex.js @@ -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> ) } diff --git a/themes/example/LayoutPage.js b/themes/example/LayoutPage.js index d8bb93ba..fc19035f 100644 --- a/themes/example/LayoutPage.js +++ b/themes/example/LayoutPage.js @@ -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> ) } diff --git a/themes/example/LayoutSearch.js b/themes/example/LayoutSearch.js index 110c11a6..c79c3ed0 100644 --- a/themes/example/LayoutSearch.js +++ b/themes/example/LayoutSearch.js @@ -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> - ) } diff --git a/themes/example/LayoutSlug.js b/themes/example/LayoutSlug.js index 91b11d80..1f7fd409 100644 --- a/themes/example/LayoutSlug.js +++ b/themes/example/LayoutSlug.js @@ -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> ) } diff --git a/themes/example/LayoutTag.js b/themes/example/LayoutTag.js index e1a549f4..e9809123 100644 --- a/themes/example/LayoutTag.js +++ b/themes/example/LayoutTag.js @@ -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 > } diff --git a/themes/example/LayoutTagIndex.js b/themes/example/LayoutTagIndex.js index 3fb41c9f..6a89e168 100644 --- a/themes/example/LayoutTagIndex.js +++ b/themes/example/LayoutTagIndex.js @@ -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> } diff --git a/themes/example/components/ArticleInfo.js b/themes/example/components/ArticleInfo.js new file mode 100644 index 00000000..f7dc5a80 --- /dev/null +++ b/themes/example/components/ArticleInfo.js @@ -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> +} diff --git a/themes/example/components/ArticleLock.js b/themes/example/components/ArticleLock.js index bfdd00ce..1f4ade0e 100644 --- a/themes/example/components/ArticleLock.js +++ b/themes/example/components/ArticleLock.js @@ -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> } diff --git a/themes/example/components/BlogList.js b/themes/example/components/BlogList.js new file mode 100644 index 00000000..7e2b49f0 --- /dev/null +++ b/themes/example/components/BlogList.js @@ -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> +} diff --git a/themes/example/components/Footer.js b/themes/example/components/Footer.js new file mode 100644 index 00000000..d94e6648 --- /dev/null +++ b/themes/example/components/Footer.js @@ -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> +} diff --git a/themes/example/components/Header.js b/themes/example/components/Header.js new file mode 100644 index 00000000..399fb5b7 --- /dev/null +++ b/themes/example/components/Header.js @@ -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> +} diff --git a/themes/example/components/JumpToTopButton.js b/themes/example/components/JumpToTopButton.js new file mode 100644 index 00000000..30e684a8 --- /dev/null +++ b/themes/example/components/JumpToTopButton.js @@ -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 diff --git a/themes/example/components/Nav.js b/themes/example/components/Nav.js new file mode 100644 index 00000000..4c3217c0 --- /dev/null +++ b/themes/example/components/Nav.js @@ -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> +} diff --git a/themes/example/components/SideBar.js b/themes/example/components/SideBar.js new file mode 100644 index 00000000..b082bc6d --- /dev/null +++ b/themes/example/components/SideBar.js @@ -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> +} diff --git a/themes/example/components/Title.js b/themes/example/components/Title.js new file mode 100644 index 00000000..b0535341 --- /dev/null +++ b/themes/example/components/Title.js @@ -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> +} diff --git a/themes/fukasawa/LayoutSearch.js b/themes/fukasawa/LayoutSearch.js index 46d01031..f3e8da7d 100644 --- a/themes/fukasawa/LayoutSearch.js +++ b/themes/fukasawa/LayoutSearch.js @@ -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>`) diff --git a/themes/fukasawa/components/ArticleLock.js b/themes/fukasawa/components/ArticleLock.js index 0d10b513..23112eeb 100644 --- a/themes/fukasawa/components/ArticleLock.js +++ b/themes/fukasawa/components/ArticleLock.js @@ -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' diff --git a/themes/fukasawa/components/AsideLeft.js b/themes/fukasawa/components/AsideLeft.js index c7f52d69..2b1f8f61 100644 --- a/themes/fukasawa/components/AsideLeft.js +++ b/themes/fukasawa/components/AsideLeft.js @@ -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'> diff --git a/themes/hexo/Layout404.js b/themes/hexo/Layout404.js index 2bc61804..f64e8016 100644 --- a/themes/hexo/Layout404.js +++ b/themes/hexo/Layout404.js @@ -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) diff --git a/themes/hexo/LayoutBase.js b/themes/hexo/LayoutBase.js index d7fdbf2b..c8ca3f8a 100644 --- a/themes/hexo/LayoutBase.js +++ b/themes/hexo/LayoutBase.js @@ -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> diff --git a/themes/hexo/LayoutSlug.js b/themes/hexo/LayoutSlug.js index e914a191..ce832a68 100644 --- a/themes/hexo/LayoutSlug.js +++ b/themes/hexo/LayoutSlug.js @@ -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> diff --git a/themes/hexo/components/ArticleLock.js b/themes/hexo/components/ArticleLock.js index b7165362..9a2c5705 100644 --- a/themes/hexo/components/ArticleLock.js +++ b/themes/hexo/components/ArticleLock.js @@ -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> } diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Header.js index 388f01a3..c1e4e71f 100644 --- a/themes/hexo/components/Header.js +++ b/themes/hexo/components/Header.js @@ -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> diff --git a/themes/hexo/components/HeaderArticle.js b/themes/hexo/components/HeaderArticle.js index bd0bea4a..8513419d 100644 --- a/themes/hexo/components/HeaderArticle.js +++ b/themes/hexo/components/HeaderArticle.js @@ -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 { diff --git a/themes/hexo/components/Logo.js b/themes/hexo/components/Logo.js index 8844d1ad..e1adeb3d 100644 --- a/themes/hexo/components/Logo.js +++ b/themes/hexo/components/Logo.js @@ -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 diff --git a/themes/hexo/components/MenuList.js b/themes/hexo/components/MenuList.js index c8103624..3f16a2ff 100644 --- a/themes/hexo/components/MenuList.js +++ b/themes/hexo/components/MenuList.js @@ -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) diff --git a/themes/hexo/components/Progress.js b/themes/hexo/components/Progress.js index 6d388339..b31ae16e 100644 --- a/themes/hexo/components/Progress.js +++ b/themes/hexo/components/Progress.js @@ -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 diff --git a/themes/hexo/components/SideRight.js b/themes/hexo/components/SideRight.js index 7d690632..55f3ccac 100644 --- a/themes/hexo/components/SideRight.js +++ b/themes/hexo/components/SideRight.js @@ -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} />} diff --git a/themes/hexo/components/TopNav.js b/themes/hexo/components/TopNav.js index 2b84e97d..25608147 100644 --- a/themes/hexo/components/TopNav.js +++ b/themes/hexo/components/TopNav.js @@ -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> diff --git a/themes/medium/LayoutSearch.js b/themes/medium/LayoutSearch.js index 59a15491..e708eb5c 100644 --- a/themes/medium/LayoutSearch.js +++ b/themes/medium/LayoutSearch.js @@ -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> } diff --git a/themes/medium/components/ArticleLock.js b/themes/medium/components/ArticleLock.js index aba09687..4b969835 100644 --- a/themes/medium/components/ArticleLock.js +++ b/themes/medium/components/ArticleLock.js @@ -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> } diff --git a/themes/medium/components/Progress.js b/themes/medium/components/Progress.js index d9a13ad9..50d2bc92 100644 --- a/themes/medium/components/Progress.js +++ b/themes/medium/components/Progress.js @@ -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 diff --git a/themes/next/Layout404.js b/themes/next/Layout404.js index ee743351..530d2c53 100644 --- a/themes/next/Layout404.js +++ b/themes/next/Layout404.js @@ -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) diff --git a/themes/next/LayoutSearch.js b/themes/next/LayoutSearch.js index 5423f60e..e3e58025 100644 --- a/themes/next/LayoutSearch.js +++ b/themes/next/LayoutSearch.js @@ -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>`) diff --git a/themes/next/components/ArticleLock.js b/themes/next/components/ArticleLock.js index b8132316..4bb2ef65 100644 --- a/themes/next/components/ArticleLock.js +++ b/themes/next/components/ArticleLock.js @@ -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> diff --git a/themes/next/components/Header.js b/themes/next/components/Header.js index 37eb5e50..df6db180 100644 --- a/themes/next/components/Header.js +++ b/themes/next/components/Header.js @@ -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"> diff --git a/themes/next/components/Progress.js b/themes/next/components/Progress.js index 4c4adb9f..19ca0ffa 100644 --- a/themes/next/components/Progress.js +++ b/themes/next/components/Progress.js @@ -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