mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
Merge remote-tracking branch 'origin/theme-Fukasawa' into theme-Next
This commit is contained in:
@@ -6,16 +6,8 @@ const BLOG = {
|
||||
email: 'tlyong1992@hotmail.com', // 联系邮箱
|
||||
link: 'https://tangly1024.com', // 网站地址
|
||||
keywords: ['Notion', '写作', '博客'], // 网站关键词
|
||||
home: { // 首页
|
||||
showHomeBanner: false, // 首页是否显示大图及标语 [true,false]
|
||||
homeBannerStrings: ['Hi,我是一个程序员', 'Hi,我是一个打工人', 'Hi,我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
|
||||
homeBannerImage: './bg_image.jpg', // 背景图地址
|
||||
showPostCover: false, // 文章列表显示封面图
|
||||
showPreview: true, // 列表展示文章预览
|
||||
previewLines: 12, // 预览文章的篇幅
|
||||
showSummary: false // 显示用户自定义摘要
|
||||
},
|
||||
lang: 'zh-CN', // ['zh-CN','en-US'] default lang => see /lib/lang.js for more.
|
||||
beian: '闽ICP备20010331号', // 备案号
|
||||
notionPageId: process.env.NOTION_PAGE_ID || 'bee1fccfa3bd47a1a7be83cc71372d83', // Important page_id!!!
|
||||
notionAccessToken: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
|
||||
appearance: 'auto', // ['light', 'dark', 'auto'],
|
||||
@@ -26,29 +18,8 @@ const BLOG = {
|
||||
since: 2020, // if leave this empty, current year will be used.
|
||||
postListStyle: 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
|
||||
postsPerPage: 6, // post counts per page
|
||||
previewLines: 12, // 预览博客行数
|
||||
sortByDate: false,
|
||||
topNavType: 'normal', // ['fixed','autoCollapse','normal'] 分别是固定顶部、固定底部滑动时自动折叠,不固定
|
||||
menu: { // 菜单栏设置
|
||||
showAbout: false, // 显示关于
|
||||
showCategory: true, // 显示分类
|
||||
showTag: true, // 显示标签
|
||||
showArchive: true, // 显示归档
|
||||
showSearch: true // 显示搜索
|
||||
},
|
||||
widget: { // 挂件及组件设置
|
||||
showPet: false, // 是否显示宠物挂件
|
||||
petLink: 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
|
||||
showToTop: true, // 是否显示回顶
|
||||
showToBottom: false, // 显示回底
|
||||
showDarkMode: false, // 显示日间/夜间模式切换
|
||||
showToc: true, // 移动端显示悬浮目录
|
||||
showShareBar: false, // 文章分享功能
|
||||
showRelatePosts: true, // 相关文章推荐
|
||||
showCopyRight: true, // 文章版权声明
|
||||
showLatestPost: false, // 右侧边栏显示最近更新
|
||||
showCategoryList: false, // 右侧边栏显示文章分类列表
|
||||
showTagList: false // 右侧边栏显示标签分类列表
|
||||
},
|
||||
socialLink: { // 社交链接,如不需要展示可以留空白,例如 weibo:''
|
||||
weibo: 'https://weibo.com/tangly1024',
|
||||
twitter: 'https://twitter.com/troy1024_1',
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 统计网站信息
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function Analytics ({ postCount }) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <>
|
||||
{/* <div className='px-5 text-sm font-light pb-1 text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faChartBar} className='mr-2' />{locale.COMMON.ANALYTICS}</div> */}
|
||||
<div className='mt-2 text-center dark:text-gray-300 font-light text-xs'>
|
||||
<span className='px-1 '>
|
||||
<strong className='font-medium'>{postCount}</strong>{locale.COMMON.POSTS}</span>
|
||||
<span className='px-1 busuanzi_container_site_uv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_uv font-medium'></strong>{locale.COMMON.VISITORS}</span>
|
||||
{/* <span className='px-1 busuanzi_container_site_pv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_pv font-medium'></strong>{locale.COMMON.VIEWS}</span> */}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const CommonHead = ({ meta }) => {
|
||||
<meta name='twitter:card' content='summary_large_image' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
{meta.type === 'article' && (
|
||||
{meta?.type === 'article' && (
|
||||
<>
|
||||
<meta
|
||||
property='article:published_time'
|
||||
|
||||
@@ -5,7 +5,7 @@ import BLOG from '@/blog.config'
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ThirdPartyScript = () => {
|
||||
const CommonScript = () => {
|
||||
return (<>
|
||||
{BLOG.comment?.DaoVoiceId && (<>
|
||||
{/* DaoVoice 反馈 */}
|
||||
@@ -109,4 +109,4 @@ const ThirdPartyScript = () => {
|
||||
</>)
|
||||
}
|
||||
|
||||
export default ThirdPartyScript
|
||||
export default CommonScript
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import Image from 'next/image'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const NavBar = () => {
|
||||
const links = []
|
||||
return (
|
||||
<div className='flex-shrink-0'>
|
||||
<ul className='flex flex-row'>
|
||||
{links.map(
|
||||
link =>
|
||||
link.show && (
|
||||
<li
|
||||
key={link.id}
|
||||
className='block ml-4 text-black dark:text-gray-50 nav'
|
||||
>
|
||||
<Link href={link.to}>
|
||||
<a>{(link.icon && (<i className={'px-1 fa ' + link.icon} />))} {link.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ navBarTitle, fullWidth = true }) => {
|
||||
const navRef = useRef(null)
|
||||
const sentinelRef = useRef([])
|
||||
// 当Header移出屏幕时改变的样式
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current.classList.remove('sticky-nav-full')
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(handler)
|
||||
observer.observe(sentinelRef.current)
|
||||
// Don't touch this, I have no idea how it works XD
|
||||
// return () => {
|
||||
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
// }
|
||||
}, [sentinelRef])
|
||||
return (
|
||||
<>
|
||||
{BLOG.autoCollapsedNavBar === true && (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var windowTop=0;
|
||||
function scrollTrigger(){
|
||||
let scrollS = window.scrollY;
|
||||
let nav = document.querySelector('.sticky-nav');
|
||||
if(scrollS >= windowTop){
|
||||
nav.style.opacity = 0;
|
||||
windowTop = scrollS;
|
||||
}else{
|
||||
nav.style.opacity = 1;
|
||||
windowTop = scrollS;
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll',scrollTrigger);
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className='observer-element h-0.5 ' ref={sentinelRef} />
|
||||
|
||||
<div
|
||||
className='z-30 px-4 sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center py-6 bg-white bg-opacity-80 '
|
||||
id='sticky-nav'
|
||||
ref={navRef}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
|
||||
<div className='flex cursor-pointer'>
|
||||
<div className='px-2 text-xl'>
|
||||
<FontAwesomeIcon icon={faBars} className='hover:scale-125 transform duration-200' />
|
||||
</div>
|
||||
<Image
|
||||
alt={BLOG.title}
|
||||
width={28}
|
||||
height={28}
|
||||
src='/avatar.jpg'
|
||||
className='rounded-full'
|
||||
/>
|
||||
<div
|
||||
className='mx-1 text-center cursor-pointer text-xl p-1
|
||||
dark:bg-gray-900 dark:text-gray-300 font-semibold
|
||||
dark:hover:bg-gray-600 text-black hover:scale-105
|
||||
hover:shadow-2xl duration-200 transform'>{BLOG.title}</div>
|
||||
|
||||
</div>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className='ml-1 font-medium text-gray-500 dark:text-night header-name'>
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className='ml-1 font-medium dark:text-night header-name'>
|
||||
<span className='font-normal'>{BLOG.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -5,7 +5,7 @@
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["components/*"],
|
||||
"@/data/*": ["data/*"],
|
||||
"@/layouts/*": ["layouts/*"],
|
||||
"@/layouts/*": ["theme/*"],
|
||||
"@/lib/*": ["lib/*"],
|
||||
"@/styles/*": ["styles/*"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import { getAllTags } from './getAllTags'
|
||||
* 获取博客数据
|
||||
* @param {*} pageId
|
||||
* @param {*} from
|
||||
* @returns
|
||||
* @param latestPostCount 截取最新文章数量
|
||||
* @param tagsCount 截取标签数量
|
||||
* @param includePage 是否包含PAGE类型
|
||||
* @returns { allPosts: '文章列表', latestPosts: ’最新文章, categories:‘分类列表’, postCount:'文章总数',tags:'标签列表' }
|
||||
* allPosts 所有博客
|
||||
* categories 所有分类
|
||||
* tags 所有标签
|
||||
|
||||
33
pages/404.js
33
pages/404.js
@@ -1,38 +1,11 @@
|
||||
import { Layout404 } from '@/themes'
|
||||
|
||||
/**
|
||||
* 自定义404界面
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
export default function Custom404 () {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
// 延时3秒如果加载失败就返回首页
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
}, 30000000)
|
||||
})
|
||||
|
||||
return <BaseLayout meta={{ title: `${BLOG.title} | 页面找不到啦` }}>
|
||||
<div
|
||||
className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
|
||||
<div className='dark:text-gray-200'>
|
||||
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'><FontAwesomeIcon icon={faSpinner} spin={true} className='mr-2'/>404</h2>
|
||||
<div className='inline-block text-left h-32 leading-10 items-center'>
|
||||
<h2 className='m-0 p-0'>页面无法加载,即将返回首页</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
return <Layout404 />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line @next/next/no-document-import-in-page
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import BLOG from '@/blog.config'
|
||||
import ThirdPartyScript from '@/components/ThirdPartyScript'
|
||||
import CommonScript from '@/components/CommonScript'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps (ctx) {
|
||||
@@ -15,10 +15,10 @@ class MyDocument extends Document {
|
||||
<Head>
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
<link rel='icon' href='/favicon.svg' type='image/svg+xml' />
|
||||
<ThirdPartyScript />
|
||||
<CommonScript />
|
||||
</Head>
|
||||
|
||||
<body className={`${BLOG.font} bg-day dark:bg-night duration-200`}>
|
||||
<body className={`${BLOG.font} bg-day dark:bg-night`}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -1,58 +1,40 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import ArticleDetail from '@/components/ArticleDetail'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import {
|
||||
getPostBlocks
|
||||
} from '@/lib/notion'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import Custom404 from '@/pages/404'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import React from 'react'
|
||||
import { LayoutSlug } from '@/themes'
|
||||
|
||||
/**
|
||||
* 关于页面,默认取notion中slug为about的文章
|
||||
* @param {*} param0
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const About = ({ post, tags, prev, next, postCount, categories }) => {
|
||||
if (!post) {
|
||||
const About = (props) => {
|
||||
if (!props.post) {
|
||||
return <Custom404 />
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
post.title = locale.NAV.ABOUT
|
||||
const meta = {
|
||||
title: `${locale.NAV.ABOUT} | ${BLOG.title} `,
|
||||
description: post.summary,
|
||||
type: 'post',
|
||||
tags: []
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
post={post}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
<ArticleDetail post={post} prev={prev} next={next} />
|
||||
</BaseLayout>
|
||||
)
|
||||
return <LayoutSlug {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'about-props'
|
||||
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true })
|
||||
const {
|
||||
allPosts,
|
||||
categories,
|
||||
tags,
|
||||
postCount,
|
||||
latestPosts
|
||||
} = await getGlobalNotionData({
|
||||
from,
|
||||
includePage: true
|
||||
})
|
||||
const post = allPosts.find(p => p.slug === 'about')
|
||||
|
||||
if (!post) {
|
||||
return { props: {}, revalidate: 1 }
|
||||
return {
|
||||
props: {},
|
||||
revalidate: 1
|
||||
}
|
||||
}
|
||||
|
||||
post.blockMap = await getPostBlocks(post.id, 'slug')
|
||||
@@ -62,7 +44,15 @@ export async function getStaticProps () {
|
||||
const next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
|
||||
return {
|
||||
props: { post, tags, prev, next, categories, postCount, latestPosts },
|
||||
props: {
|
||||
post,
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
},
|
||||
revalidate: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostArchive from '@/components/BlogPostArchive'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { LayoutArchive } from '@/themes'
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'index'
|
||||
const { allPosts, categories, tags, postCount } =
|
||||
await getGlobalNotionData({ from })
|
||||
await getGlobalNotionData({ from: 'archive-index' })
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -22,63 +17,8 @@ export async function getStaticProps () {
|
||||
}
|
||||
}
|
||||
|
||||
const Index = ({ posts, tags, categories, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
// 深拷贝
|
||||
const postsSortByDate = Object.create(posts)
|
||||
|
||||
// 时间排序
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date.start_date.slice(0, 7)
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
archivePosts[date] = [post]
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BaseLayout meta={meta} tags={tags} categories={categories} postCount={postCount}>
|
||||
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogPostArchive
|
||||
key={archiveTitle}
|
||||
posts={archivePosts[archiveTitle]}
|
||||
archiveTitle={archiveTitle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Live2D />
|
||||
</BaseLayout>
|
||||
)
|
||||
const ArchiveIndex = (props) => {
|
||||
return <LayoutArchive {...props}/>
|
||||
}
|
||||
|
||||
export default Index
|
||||
export default ArchiveIndex
|
||||
|
||||
@@ -1,78 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import ArticleDetail from '@/components/ArticleDetail'
|
||||
import Card from '@/components/Card'
|
||||
import LatestPostsGroup from '@/components/LatestPostsGroup'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import TocDrawer from '@/components/TocDrawer'
|
||||
import TocDrawerButton from '@/components/TocDrawerButton'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { LayoutSlug } from '@/themes'
|
||||
import Custom404 from '@/pages/404'
|
||||
import { getPageTableOfContents } from 'notion-utils'
|
||||
import { useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
* @param {*} param0
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Slug = ({
|
||||
post,
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
if (!post) {
|
||||
const Slug = (props) => {
|
||||
if (!props.post) {
|
||||
return <Custom404 />
|
||||
}
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.title}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
const drawerRight = useRef(null)
|
||||
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
|
||||
post.content = Object.keys(post?.blockMap?.block)
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
const floatSlot = post?.toc?.length > 1 ? <div className="block lg:hidden"><TocDrawerButton onClick={() => { drawerRight?.current?.handleSwitchVisible() }} /></div> : null
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
post={post}
|
||||
postCount={postCount}
|
||||
latestPosts={latestPosts}
|
||||
categories={categories}
|
||||
floatSlot={floatSlot}
|
||||
rightAreaSlot={
|
||||
BLOG.widget?.showLatestPost && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
>
|
||||
<ArticleDetail
|
||||
post={post}
|
||||
recommendPosts={recommendPosts}
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
|
||||
{/* 悬浮目录按钮 */}
|
||||
<div className="block lg:hidden">
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
|
||||
{/* 宠物 */}
|
||||
<Live2D/>
|
||||
|
||||
</BaseLayout>
|
||||
)
|
||||
return <LayoutSlug {...props}/>
|
||||
}
|
||||
|
||||
export async function getStaticPaths () {
|
||||
@@ -83,8 +24,8 @@ export async function getStaticPaths () {
|
||||
}
|
||||
}
|
||||
|
||||
const from = '[slug-paths'
|
||||
const { allPosts } = await getGlobalNotionData({ from, includePage: false })
|
||||
const from = 'slug-paths'
|
||||
const { allPosts } = await getGlobalNotionData({ from, includePage: true })
|
||||
return {
|
||||
paths: allPosts.map(row => ({ params: { slug: row.slug } })),
|
||||
fallback: true
|
||||
@@ -94,7 +35,7 @@ export async function getStaticPaths () {
|
||||
export async function getStaticProps ({ params: { slug } }) {
|
||||
const from = `slug-props-${slug}`
|
||||
const { allPosts, categories, tags, postCount, latestPosts } =
|
||||
await getGlobalNotionData({ from, includePage: false })
|
||||
await getGlobalNotionData({ from, includePage: true })
|
||||
|
||||
const post = allPosts.find(p => p.slug === slug)
|
||||
|
||||
@@ -128,7 +69,7 @@ export async function getStaticProps ({ params: { slug } }) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {获取文章推荐文章} post
|
||||
* @param post
|
||||
* @param {*} allPosts
|
||||
* @param {*} count
|
||||
* @returns
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListScroll from '@/components/BlogPostListScroll'
|
||||
import CategoryList from '@/components/CategoryList'
|
||||
import StickyBar from '@/components/StickyBar'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import React from 'react'
|
||||
import { LayoutCategory } from '@/themes'
|
||||
|
||||
export default function Category ({ tags, posts, category, categories, latestPosts, postCount }) {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <BaseLayout meta={meta} tags={tags} currentCategory={category} postCount={postCount} latestPosts={latestPosts} categories={categories}>
|
||||
<StickyBar>
|
||||
<CategoryList currentCategory={category} categories={categories} />
|
||||
</StickyBar>
|
||||
<div className='md:mt-8'>
|
||||
<BlogPostListScroll posts={posts} tags={tags} currentCategory={category}/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
export default function Category (props) {
|
||||
return <LayoutCategory {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps ({ params }) {
|
||||
const from = 'category-props'
|
||||
const category = params.category
|
||||
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from })
|
||||
const {
|
||||
allPosts,
|
||||
categories,
|
||||
tags,
|
||||
postCount,
|
||||
latestPosts
|
||||
} = await getGlobalNotionData({ from })
|
||||
const filteredPosts = allPosts.filter(
|
||||
post => post && post.category && post.category.includes(category)
|
||||
)
|
||||
|
||||
@@ -1,32 +1,9 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { faFolder, faThList } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { LayoutCategoryIndex } from '@/themes'
|
||||
|
||||
export default function Category ({ tags, allPosts, categories, postCount, latestPosts }) {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <BaseLayout meta={meta} totalPosts={allPosts} tags={tags} postCount={postCount} latestPosts={latestPosts}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faThList} className='mr-4' />{locale.COMMON.CATEGORY}:</div>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{Object.keys(categories).map(category => {
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<div className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})</div>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
export default function Category (props) {
|
||||
return <LayoutCategoryIndex {...props}/>
|
||||
}
|
||||
|
||||
export async function getStaticProps () {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListPage from '@/components/BlogPostListPage'
|
||||
import BlogPostListScroll from '@/components/BlogPostListScroll'
|
||||
import Card from '@/components/Card'
|
||||
import Header from '@/components/Header'
|
||||
import LatestPostsGroup from '@/components/LatestPostsGroup'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { LayoutIndex } from '@/themes'
|
||||
|
||||
const Index = (props) => {
|
||||
return <LayoutIndex {...props}/>
|
||||
}
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'index'
|
||||
@@ -19,7 +18,7 @@ export async function getStaticProps () {
|
||||
|
||||
// 处理分页
|
||||
const page = 1
|
||||
let postsToShow = []
|
||||
let postsToShow
|
||||
if (BLOG.postListStyle !== 'page') {
|
||||
postsToShow = Array.from(allPosts)
|
||||
} else {
|
||||
@@ -29,7 +28,7 @@ export async function getStaticProps () {
|
||||
)
|
||||
for (const i in postsToShow) {
|
||||
const post = postsToShow[i]
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.home.previewLines)
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.previewLines)
|
||||
if (blockMap) {
|
||||
post.blockMap = blockMap
|
||||
}
|
||||
@@ -49,28 +48,4 @@ export async function getStaticProps () {
|
||||
}
|
||||
}
|
||||
|
||||
const Index = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
return (
|
||||
<BaseLayout
|
||||
headerSlot={BLOG.home.showHomeBanner && <Header />}
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
|
||||
rightAreaSlot={
|
||||
BLOG.widget?.showLatestPost && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
{BLOG.postListStyle !== 'page'
|
||||
? (
|
||||
<BlogPostListScroll posts={posts} tags={tags} showSummary={true} />
|
||||
)
|
||||
: (
|
||||
<BlogPostListPage posts={posts} tags={tags} postCount={postCount} />
|
||||
)}
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListPage from '@/components/BlogPostListPage'
|
||||
import Header from '@/components/Header'
|
||||
import LatestPostsGroup from '@/components/LatestPostsGroup'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import Custom404 from '../404'
|
||||
import { LayoutPage } from '@/themes'
|
||||
import Custom404 from '@/pages/404'
|
||||
|
||||
const Page = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
if (!meta || BLOG.postListStyle !== 'page') {
|
||||
return <Custom404/>
|
||||
const Page = (props) => {
|
||||
if (!props?.meta) {
|
||||
return <Custom404 />
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
headerSlot={BLOG.home.showHomeBanner && <Header />}
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
|
||||
rightAreaSlot={BLOG.widget?.showLatestPost && <LatestPostsGroup posts={latestPosts} />}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
|
||||
</BaseLayout>
|
||||
)
|
||||
return <LayoutPage {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticPaths () {
|
||||
@@ -40,12 +24,19 @@ export async function getStaticPaths () {
|
||||
|
||||
export async function getStaticProps ({ params: { page } }) {
|
||||
const from = `page-${page}`
|
||||
const { allPosts, latestPosts, categories, tags, postCount } = await getGlobalNotionData({ from })
|
||||
const {
|
||||
allPosts,
|
||||
latestPosts,
|
||||
categories,
|
||||
tags,
|
||||
postCount
|
||||
} = await getGlobalNotionData({ from })
|
||||
const meta = {
|
||||
title: `${page} | Page | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
// 处理分页
|
||||
const postsToShow = allPosts.slice(
|
||||
BLOG.postsPerPage * (page - 1),
|
||||
@@ -54,7 +45,7 @@ export async function getStaticProps ({ params: { page } }) {
|
||||
|
||||
for (const i in postsToShow) {
|
||||
const post = postsToShow[i]
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.home.previewLines)
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.previewLines)
|
||||
if (blockMap) {
|
||||
post.blockMap = blockMap
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListScroll from '@/components/BlogPostListScroll'
|
||||
import StickyBar from '@/components/StickyBar'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LayoutSearch } from '@/themes'
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'search-props'
|
||||
const { allPosts, categories, tags, postCount, latestPosts } =
|
||||
await getGlobalNotionData({ from })
|
||||
|
||||
const {
|
||||
allPosts,
|
||||
categories,
|
||||
tags,
|
||||
postCount,
|
||||
latestPosts
|
||||
} = await getGlobalNotionData({ from: 'search-props' })
|
||||
return {
|
||||
props: {
|
||||
posts: allPosts,
|
||||
@@ -25,51 +21,8 @@ export async function getStaticProps () {
|
||||
}
|
||||
}
|
||||
|
||||
const Search = ({ posts, tags, categories, postCount }) => {
|
||||
let filteredPosts = []
|
||||
const searchKey = getSearchKey()
|
||||
if (searchKey) {
|
||||
filteredPosts = posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredPosts = posts
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.title} `,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return (
|
||||
<BaseLayout
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
postCount={postCount}
|
||||
currentSearch={searchKey}
|
||||
categories={categories}
|
||||
>
|
||||
<StickyBar>
|
||||
<div className="p-4 dark:text-gray-200">
|
||||
<FontAwesomeIcon icon={faSearch} className="mr-1" />{' '}
|
||||
{filteredPosts.length} {locale.COMMON.RESULT_OF_SEARCH}
|
||||
</div>
|
||||
</StickyBar>
|
||||
<div className="md:mt-5">
|
||||
<BlogPostListScroll posts={filteredPosts} tags={tags} showSummary={true}/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
)
|
||||
const Search = (props) => {
|
||||
return <LayoutSearch {...props} />
|
||||
}
|
||||
|
||||
export function getSearchKey () {
|
||||
const router = useRouter()
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
export default Search
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListScroll from '@/components/BlogPostListScroll'
|
||||
import StickyBar from '@/components/StickyBar'
|
||||
import TagList from '@/components/TagList'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { LayoutTag } from '@/themes'
|
||||
|
||||
export default function Tag ({ tags, posts, tag, categories, postCount, latestPosts }) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
// 将当前选中的标签置顶🔝
|
||||
if (!tags) tags = []
|
||||
const currentTag = tags?.find(r => r?.name === tag)
|
||||
const newTags = currentTag ? [currentTag].concat(tags.filter(r => r?.name !== tag)) : tags.filter(r => r?.name !== tag)
|
||||
|
||||
return <BaseLayout meta={meta} tags={tags} currentTag={tag} categories={categories} postCount={postCount} latestPosts={latestPosts}>
|
||||
<StickyBar>
|
||||
<TagList tags={newTags} currentTag={tag}/>
|
||||
</StickyBar>
|
||||
<div className='md:mt-8'>
|
||||
<BlogPostListScroll posts={posts} tags={tags} currentTag={tag}/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
const Tag = (props) => {
|
||||
return <LayoutTag {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps ({ params }) {
|
||||
const tag = params.tag
|
||||
const from = 'tag-props'
|
||||
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
|
||||
const {
|
||||
allPosts,
|
||||
categories,
|
||||
tags,
|
||||
postCount,
|
||||
latestPosts
|
||||
} = await getGlobalNotionData({
|
||||
from,
|
||||
includePage: true,
|
||||
tagsCount: 0
|
||||
})
|
||||
const filteredPosts = allPosts.filter(
|
||||
post => post && post.tags && post.tags.includes(tag)
|
||||
)
|
||||
@@ -52,8 +37,8 @@ export async function getStaticProps ({ params }) {
|
||||
|
||||
/**
|
||||
* 获取所有的标签
|
||||
* @param {*}} allPosts
|
||||
* @returns
|
||||
* @param tags
|
||||
*/
|
||||
function getTagNames (tags) {
|
||||
const tagNames = []
|
||||
@@ -65,7 +50,10 @@ function getTagNames (tags) {
|
||||
|
||||
export async function getStaticPaths () {
|
||||
const from = 'tag-static-path'
|
||||
const { tags } = await getGlobalNotionData({ from, tagsCount: 0 })
|
||||
const { tags } = await getGlobalNotionData({
|
||||
from,
|
||||
tagsCount: 0
|
||||
})
|
||||
const tagNames = getTagNames(tags)
|
||||
|
||||
return {
|
||||
@@ -73,3 +61,5 @@ export async function getStaticPaths () {
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
export default Tag
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import TagItem from '@/components/TagItem'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
import { LayoutTagIndex } from '@/themes'
|
||||
|
||||
export default function Tag ({ tags, categories, postCount, latestPosts }) {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <BaseLayout meta={meta} categories={categories} postCount={postCount} latestPosts={latestPosts}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTags} className='mr-4'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{ tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
const TagIndex = (props) => {
|
||||
return <LayoutTagIndex {...props} />
|
||||
}
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'tag-index-props'
|
||||
const { categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
|
||||
const {
|
||||
categories,
|
||||
tags,
|
||||
postCount,
|
||||
latestPosts
|
||||
} = await getGlobalNotionData({
|
||||
from,
|
||||
includePage: true,
|
||||
tagsCount: 0
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -40,3 +29,5 @@ export async function getStaticProps () {
|
||||
revalidate: 1
|
||||
}
|
||||
}
|
||||
|
||||
export default TagIndex
|
||||
|
||||
@@ -8,7 +8,7 @@ const fontSerifCJK = !CJK()
|
||||
? []
|
||||
: [`"Noto Serif CJK ${CJK()}"`, `"Noto Serif ${CJK()}"`]
|
||||
module.exports = {
|
||||
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js'],
|
||||
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.js'],
|
||||
darkMode: BLOG.appearance === 'class' ? 'media' : 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
|
||||
6
themes/Empty/Layout404.js
Normal file
6
themes/Empty/Layout404.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
export const Layout404 = () => {
|
||||
return <div>
|
||||
404 Not found.
|
||||
</div>
|
||||
}
|
||||
5
themes/Empty/LayoutArchive.js
Normal file
5
themes/Empty/LayoutArchive.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
|
||||
return <div>
|
||||
Archive Page
|
||||
</div>
|
||||
}
|
||||
40
themes/Empty/LayoutBase.js
Normal file
40
themes/Empty/LayoutBase.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param children
|
||||
* @param layout
|
||||
* @param tags
|
||||
* @param meta
|
||||
* @param post
|
||||
* @param currentSearch
|
||||
* @param currentCategory
|
||||
* @param currentTag
|
||||
* @param categories
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = ({
|
||||
children,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
post,
|
||||
postCount,
|
||||
sideBarSlot,
|
||||
floatSlot,
|
||||
rightAreaSlot,
|
||||
currentSearch,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories
|
||||
}) => {
|
||||
return (<>
|
||||
<CommonHead meta={meta} />
|
||||
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
|
||||
{children}
|
||||
</main>
|
||||
</>)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
5
themes/Empty/LayoutCategory.js
Normal file
5
themes/Empty/LayoutCategory.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
|
||||
return <div>
|
||||
Category - {category}
|
||||
</div>
|
||||
}
|
||||
11
themes/Empty/LayoutCategoryIndex.js
Normal file
11
themes/Empty/LayoutCategoryIndex.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const LayoutCategoryIndex = ({
|
||||
tags,
|
||||
allPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
return <div>
|
||||
CategoryIndex
|
||||
</div>
|
||||
}
|
||||
3
themes/Empty/LayoutIndex.js
Normal file
3
themes/Empty/LayoutIndex.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
return <div>Index</div>
|
||||
}
|
||||
5
themes/Empty/LayoutPage.js
Normal file
5
themes/Empty/LayoutPage.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
return <div>
|
||||
Page - {page}
|
||||
</div>
|
||||
}
|
||||
34
themes/Empty/LayoutSearch.js
Normal file
34
themes/Empty/LayoutSearch.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const LayoutSearch = ({
|
||||
posts,
|
||||
tags,
|
||||
categories,
|
||||
postCount
|
||||
}) => {
|
||||
let filteredPosts
|
||||
const searchKey = getSearchKey()
|
||||
if (searchKey) {
|
||||
filteredPosts = posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredPosts = posts
|
||||
}
|
||||
|
||||
console.log(filteredPosts)
|
||||
|
||||
return <div>
|
||||
Search {searchKey}
|
||||
</div>
|
||||
}
|
||||
|
||||
function getSearchKey () {
|
||||
const router = useRouter()
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
21
themes/Empty/LayoutSlug.js
Normal file
21
themes/Empty/LayoutSlug.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
|
||||
export const LayoutSlug = ({
|
||||
post,
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
return <div>
|
||||
Slug
|
||||
</div>
|
||||
}
|
||||
5
themes/Empty/LayoutTag.js
Normal file
5
themes/Empty/LayoutTag.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts }) => {
|
||||
return <div>
|
||||
Tag - {tag}
|
||||
</div>
|
||||
}
|
||||
5
themes/Empty/LayoutTagIndex.js
Normal file
5
themes/Empty/LayoutTagIndex.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
|
||||
return <div>
|
||||
TagIndex
|
||||
</div>
|
||||
}
|
||||
0
themes/Empty/config_empty.js
Normal file
0
themes/Empty/config_empty.js
Normal file
10
themes/Empty/index.js
Normal file
10
themes/Empty/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export { LayoutIndex } from './LayoutIndex'
|
||||
export { LayoutSearch } from './LayoutSearch'
|
||||
export { LayoutArchive } from './LayoutArchive'
|
||||
export { LayoutSlug } from './LayoutSlug'
|
||||
export { Layout404 } from './Layout404'
|
||||
export { LayoutCategory } from './LayoutCategory'
|
||||
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
|
||||
export { LayoutPage } from './LayoutPage'
|
||||
export { LayoutTag } from './LayoutTag'
|
||||
export { LayoutTagIndex } from './LayoutTagIndex'
|
||||
5
themes/Fukasawa/Layout404.js
Normal file
5
themes/Fukasawa/Layout404.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const Layout404 = (props) => {
|
||||
return <LayoutBase {...props}>404</LayoutBase>
|
||||
}
|
||||
61
themes/Fukasawa/LayoutArchive.js
Normal file
61
themes/Fukasawa/LayoutArchive.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
import BlogArchiveItem from './components/BlogPostArchive'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutArchive = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { posts } = props
|
||||
// 深拷贝
|
||||
const postsSortByDate = Object.create(posts)
|
||||
|
||||
// 时间排序
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date.start_date.slice(0, 7)
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
archivePosts[date] = [post]
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogArchiveItem
|
||||
key={archiveTitle}
|
||||
posts={archivePosts[archiveTitle]}
|
||||
archiveTitle={archiveTitle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
45
themes/Fukasawa/LayoutBase.js
Normal file
45
themes/Fukasawa/LayoutBase.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import TopNav from './components/TopNav'
|
||||
import AsideLeft from './components/AsideLeft'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param children
|
||||
* @param layout
|
||||
* @param tags
|
||||
* @param meta
|
||||
* @param post
|
||||
* @param currentSearch
|
||||
* @param currentCategory
|
||||
* @param currentTag
|
||||
* @param categories
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = (props) => {
|
||||
const {
|
||||
children,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories
|
||||
} = props
|
||||
return (<>
|
||||
<CommonHead meta={meta} />
|
||||
<TopNav {...props}/>
|
||||
<div className='flex'>
|
||||
<AsideLeft tags={tags} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
|
||||
<main id='wrapper' className='flex w-full py-8 justify-center'>
|
||||
<div className='2xl:max-w-6xl md:max-w-3xl w-full'>
|
||||
<div> {headerSlot} </div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</>)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
8
themes/Fukasawa/LayoutCategory.js
Normal file
8
themes/Fukasawa/LayoutCategory.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategory = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
<BlogListPage page={props.page} posts={props.posts} postCount={props.postCount} />
|
||||
</LayoutBase>
|
||||
}
|
||||
32
themes/Fukasawa/LayoutCategoryIndex.js
Normal file
32
themes/Fukasawa/LayoutCategoryIndex.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faFolder, faTh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategoryIndex = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { categories } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'>
|
||||
<FontAwesomeIcon icon={faTh} className='mr-4' />{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{Object.keys(categories).map(category => {
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<div
|
||||
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})
|
||||
</div>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</div> </LayoutBase>
|
||||
}
|
||||
10
themes/Fukasawa/LayoutIndex.js
Normal file
10
themes/Fukasawa/LayoutIndex.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutIndex = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
|
||||
<BlogListPage posts={props.posts} postCount={props.postCount}/>
|
||||
|
||||
</LayoutBase>
|
||||
}
|
||||
10
themes/Fukasawa/LayoutPage.js
Normal file
10
themes/Fukasawa/LayoutPage.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutPage = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
|
||||
<BlogListPage page={props.page} posts={props.posts} postCount={props.postCount}/>
|
||||
|
||||
</LayoutBase>
|
||||
}
|
||||
30
themes/Fukasawa/LayoutSearch.js
Normal file
30
themes/Fukasawa/LayoutSearch.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSearch = (props) => {
|
||||
let filteredPosts
|
||||
const searchKey = getSearchKey()
|
||||
if (searchKey) {
|
||||
filteredPosts = props.posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredPosts = props.posts
|
||||
}
|
||||
|
||||
console.log(filteredPosts)
|
||||
|
||||
return <LayoutBase {...props}>
|
||||
Search {searchKey}
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
function getSearchKey () {
|
||||
const router = useRouter()
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
25
themes/Fukasawa/LayoutSlug.js
Normal file
25
themes/Fukasawa/LayoutSlug.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import ArticleDetail from './components/ArticleDetail'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSlug = (props) => {
|
||||
const { post } = props
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.title}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase meta={meta} {...props} >
|
||||
<ArticleDetail {...props} />
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
8
themes/Fukasawa/LayoutTag.js
Normal file
8
themes/Fukasawa/LayoutTag.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTag = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
<BlogListPage {...props} />
|
||||
</LayoutBase>
|
||||
}
|
||||
27
themes/Fukasawa/LayoutTagIndex.js
Normal file
27
themes/Fukasawa/LayoutTagIndex.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import TagItem from './components/TagItem'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTagIndex = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { tags } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTag} className='mr-4'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{ tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
26
themes/Fukasawa/components/ArticleAround.js
Normal file
26
themes/Fukasawa/components/ArticleAround.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Link from 'next/link'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 上一篇,下一篇文章
|
||||
* @param {prev,next} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleAround ({ prev, next }) {
|
||||
if (!prev || !next) {
|
||||
return <></>
|
||||
}
|
||||
return <section className='text-gray-800 h-28 flex items-center justify-between space-x-5 my-4'>
|
||||
<Link href={`/article/${prev.slug}`} passHref>
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/article/${next.slug}`} passHref>
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>{next.title}
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
}
|
||||
156
themes/Fukasawa/components/ArticleDetail.js
Normal file
156
themes/Fukasawa/components/ArticleDetail.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import Comment from '@/components/Comment'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
import Link from 'next/link'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import ArticleAround from './ArticleAround'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
|
||||
|
||||
const zoom = typeof window !== 'undefined' && mediumZoom({
|
||||
container: '.notion-viewport',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
margin: getMediumZoomMargin()
|
||||
})
|
||||
const zoomRef = useRef(zoom ? zoom.clone() : null)
|
||||
|
||||
useEffect(() => {
|
||||
// 将所有container下的所有图片添加medium-zoom
|
||||
const container = document.getElementById('container')
|
||||
const imgList = container.getElementsByTagName('img')
|
||||
if (imgList && zoomRef.current) {
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
(zoomRef.current).attach(imgList[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
</div>
|
||||
)}
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-32 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
|
||||
<header className='animate__slideInDown animate__animated'>
|
||||
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
|
||||
{post.type[0] !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
|
||||
{date}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
</>)}
|
||||
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<FontAwesomeIcon icon={faEye} className='mr-1'/>
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv"/>
|
||||
<span className='mr-2'>|</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</header>
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-1'>
|
||||
{post.blockMap && (
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138"/>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
|
||||
<ArticleAround prev={prev} next={next}/>
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="duration-200 shadow px-12 w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div className='text-2xl mt-8 mx-8'>发表评论</div>
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
function getMediumZoomMargin () {
|
||||
const width = window.innerWidth
|
||||
|
||||
if (width < 500) {
|
||||
return 8
|
||||
} else if (width < 800) {
|
||||
return 20
|
||||
} else if (width < 1280) {
|
||||
return 30
|
||||
} else if (width < 1600) {
|
||||
return 40
|
||||
} else if (width < 1920) {
|
||||
return 48
|
||||
} else {
|
||||
return 72
|
||||
}
|
||||
}
|
||||
47
themes/Fukasawa/components/AsideLeft.js
Normal file
47
themes/Fukasawa/components/AsideLeft.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Logo from './Logo'
|
||||
import GroupCategory from './GroupCategory'
|
||||
import GroupMenu from './GroupMenu'
|
||||
import GroupTag from './GroupTag'
|
||||
import SearchInput from './SearchInput'
|
||||
import SiteInfo from './SiteInfo'
|
||||
|
||||
function AsideLeft ({ tags, currentTag, categories, currentCategory }) {
|
||||
return <div className='w-72 bg-white min-h-screen px-10 py-14 hidden lg:block'>
|
||||
|
||||
<Logo />
|
||||
|
||||
<section className='flex flex-col text-gray-600'>
|
||||
<hr className='w-12 my-8' />
|
||||
<GroupMenu/>
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col text-gray-600'>
|
||||
<hr className='w-12 my-8' />
|
||||
<SearchInput/>
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col'>
|
||||
<hr className='w-12 my-8 ' />
|
||||
{ BLOG.description }
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col'>
|
||||
<hr className='w-12 my-8 ' />
|
||||
<GroupTag tags={tags} currentTag={currentTag}/>
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col'>
|
||||
<hr className='w-12 my-8 ' />
|
||||
<GroupCategory categories={categories} currentCategory={currentCategory}/>
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col'>
|
||||
<hr className='w-12 my-8 ' />
|
||||
<SiteInfo/>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
export default AsideLeft
|
||||
39
themes/Fukasawa/components/BlogCard.js
Normal file
39
themes/Fukasawa/components/BlogCard.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import CONFIG_FUKA from '../config_fuka'
|
||||
import Card from './Card'
|
||||
|
||||
const BlogCard = ({ post, showSummary }) => {
|
||||
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<Card className='w-full lg:max-w-sm p-2'>
|
||||
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
|
||||
|
||||
<div className='p-2 flex flex-col w-full'>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className={`cursor-pointer font-bold hover:underline text-xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
|
||||
{post.title}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{(!showPreview || showSummary) && <p className='mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden'>
|
||||
{post.summary}
|
||||
</p>}
|
||||
</div>
|
||||
|
||||
{CONFIG_FUKA.POST_LIST_COVER && post?.page_cover && (
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<div className='h-40 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
|
||||
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div >
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogCard
|
||||
12
themes/Fukasawa/components/BlogListEmpty.js
Normal file
12
themes/Fukasawa/components/BlogListEmpty.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
/**
|
||||
* 空白博客 列表
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogListEmpty = ({ currentSearch }) => {
|
||||
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
|
||||
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
|
||||
</div>
|
||||
}
|
||||
export default BlogListEmpty
|
||||
55
themes/Fukasawa/components/BlogListPage.js
Normal file
55
themes/Fukasawa/components/BlogListPage.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import BlogCard from './BlogCard'
|
||||
import BlogPostListEmpty from './BlogListEmpty'
|
||||
import PaginationSimple from './PaginationSimple'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
* @param page 当前页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogListPage = ({ page = 1, posts = [], postCount }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
|
||||
const showNext = page < totalPage && posts.length < postCount
|
||||
const [colCount, changeCol] = useState(3)
|
||||
|
||||
function updateCol () {
|
||||
if (window.outerWidth > 1200) {
|
||||
changeCol(3)
|
||||
} else {
|
||||
changeCol(1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateCol()
|
||||
window.addEventListener('resize', updateCol)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateCol)
|
||||
}
|
||||
})
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container">
|
||||
{/* 文章列表 */}
|
||||
<div style={{ columnCount: colCount }}>
|
||||
{posts.map(post => (
|
||||
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard key={post.id} post={post} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PaginationSimple page={page} showNext={showNext} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogListPage
|
||||
32
themes/Fukasawa/components/BlogPostArchive.js
Normal file
32
themes/Fukasawa/components/BlogPostArchive.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
* 博客归档
|
||||
* @param posts 所有文章
|
||||
* @param archiveTitle 归档标题
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogArchiveItem = ({ posts = [], archiveTitle }) => {
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return <div>
|
||||
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
<li key={post.id} className='border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500'>
|
||||
<div name={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className='dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600'>{post.title}</a>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogArchiveItem
|
||||
9
themes/Fukasawa/components/Card.js
Normal file
9
themes/Fukasawa/components/Card.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow mb-4 p-2 bg-white dark:bg-gray-800 hover:shadow-lg duration-200">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
export default Card
|
||||
28
themes/Fukasawa/components/GroupCategory.js
Normal file
28
themes/Fukasawa/components/GroupCategory.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
function GroupCategory ({ currentCategory, categories }) {
|
||||
if (!categories) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <>
|
||||
<div id='category-list' className='dark:border-gray-600 flex flex-wrap'>
|
||||
{Object.keys(categories).map(category => {
|
||||
const selected = currentCategory === category
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<a className={(selected
|
||||
? 'hover:text-white dark:hover:text-white bg-gray-600 text-white '
|
||||
: 'dark:text-gray-400 text-gray-500 hover:text-white hover:bg-gray-500 dark:hover:text-white') +
|
||||
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
|
||||
<FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={`${selected ? 'text-white' : 'text-gray-400'} mr-2`} />{category}({categories[category]})
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default GroupCategory
|
||||
38
themes/Fukasawa/components/GroupMenu.js
Normal file
38
themes/Fukasawa/components/GroupMenu.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_FUKA from '../config_fuka'
|
||||
|
||||
function GroupMenu () {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
const links = [
|
||||
{ id: 0, name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ id: 1, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_FUKA.MENU_CATEGORY },
|
||||
{ id: 2, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_FUKA.MENU_TAG },
|
||||
{ id: 3, name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_FUKA.MENU_ARCHIVE },
|
||||
{ id: 4, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_FUKA.MENU_ABOUT }
|
||||
]
|
||||
return <nav id='nav' className='font-sans text-sm'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
||||
<a className={'py-0.5 duration-500 justify-between text-gray-500 hover:text-black cursor-pointer flex flex-nowrap items-center ' +
|
||||
(selected ? 'text-black' : ' ')} >
|
||||
<div className='my-auto items-center justify-center flex '>
|
||||
<div className={ 'text-gray-500 hover:text-black'}>{link.name}</div>
|
||||
</div>
|
||||
{link.slot}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
|
||||
export default GroupMenu
|
||||
24
themes/Fukasawa/components/GroupTag.js
Normal file
24
themes/Fukasawa/components/GroupTag.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
/**
|
||||
* 标签组
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function GroupTag ({ tags, currentTag }) {
|
||||
if (!tags) return <></>
|
||||
return (
|
||||
<div id='tags-group' className='dark:border-gray-600 w-66 space-y-2'>
|
||||
{
|
||||
tags.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupTag
|
||||
12
themes/Fukasawa/components/Logo.js
Normal file
12
themes/Fukasawa/components/Logo.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
|
||||
function Logo () {
|
||||
return <section className='flex'>
|
||||
<Link href='/'>
|
||||
<a className='hover:bg-black hover:text-white border-black border-2 duration-500 px-4 py-2 cursor-pointer font-black'>{BLOG.title}</a>
|
||||
</Link>
|
||||
</section>
|
||||
}
|
||||
|
||||
export default Logo
|
||||
42
themes/Fukasawa/components/PaginationSimple.js
Normal file
42
themes/Fukasawa/components/PaginationSimple.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 简易翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationSimple = ({ page, showNext }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const currentPage = +page
|
||||
return (
|
||||
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
|
||||
<Link
|
||||
href={ {
|
||||
pathname: (currentPage === 2 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
|
||||
} } passHref >
|
||||
<a
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'} 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 ? 'block' : '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>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationSimple
|
||||
68
themes/Fukasawa/components/SearchInput.js
Normal file
68
themes/Fukasawa/components/SearchInput.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
const [searchKey, setSearchKey] = useState(currentSearch || '')
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef()
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleSearch = (key) => {
|
||||
if (key && key !== '') {
|
||||
setLoadingState(true)
|
||||
router.push({ pathname: '/search', query: { s: key } }).then(r => {
|
||||
setLoadingState(false)
|
||||
})
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) { // 回车
|
||||
handleSearch(searchInputRef.current.value)
|
||||
} else if (e.keyCode === 27) { // ESC
|
||||
cleanSearch()
|
||||
}
|
||||
}
|
||||
const cleanSearch = () => {
|
||||
searchInputRef.current.value = ''
|
||||
setSearchKey('')
|
||||
}
|
||||
|
||||
const updateSearchKey = (val) => {
|
||||
setSearchKey(val)
|
||||
}
|
||||
|
||||
return <div className='flex w-full bg-gray-100'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={searchKey}
|
||||
/>
|
||||
|
||||
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
|
||||
onClick={() => { handleSearch(searchKey) }}>
|
||||
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
|
||||
</div>
|
||||
|
||||
{(searchKey && searchKey.length &&
|
||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
26
themes/Fukasawa/components/SiteInfo.js
Normal file
26
themes/Fukasawa/components/SiteInfo.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { faEye, faShieldAlt, faUsers } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
function SiteInfo ({ title }) {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const startYear = BLOG.since && BLOG.since !== currentYear && BLOG.since + '-'
|
||||
return (
|
||||
<footer
|
||||
className='leading-6 justify-start w-full text-gray-400 text-xs font-sans'
|
||||
>
|
||||
<span> © {`${startYear}${currentYear}`} <span> <a href={BLOG.link} className='text-gray-500 dark:text-gray-300 '>{BLOG.author}</a>. <br /></span>
|
||||
|
||||
<span>Powered by <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span><br /></span>
|
||||
|
||||
{BLOG.beian && <><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.beian}</a><br/></>}
|
||||
|
||||
<span className='hidden busuanzi_container_site_pv'> <FontAwesomeIcon icon={faEye} /><span className='px-1 busuanzi_value_site_pv'> </span> </span>
|
||||
<span className='pl-2 hidden busuanzi_container_site_uv'> <FontAwesomeIcon icon={faUsers} /> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
|
||||
<br />
|
||||
<h1>{title}</h1>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
export default SiteInfo
|
||||
@@ -7,7 +7,7 @@ import { useGlobal } from '@/lib/global'
|
||||
const TagItem = ({ tag, selected }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (!tag) {
|
||||
<>{locale.COMMON.NOTAG}</>
|
||||
<div> { locale.COMMON.NOTAG } </div>
|
||||
}
|
||||
return (
|
||||
<Link href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
51
themes/Fukasawa/components/TopNav.js
Normal file
51
themes/Fukasawa/components/TopNav.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useState } from 'react'
|
||||
import Collapse from './Collapse'
|
||||
import GroupMenu from './GroupMenu'
|
||||
import Logo from './Logo'
|
||||
|
||||
/**
|
||||
* 顶部导航
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => {
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
return (<div id='top-nav' className='z-40 block lg:hidden'>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'lg:relative w-full top-0 z-20 transform duration-500'}>
|
||||
<Collapse isOpen={isOpen}>
|
||||
<div className='bg-white py-1 px-5'>
|
||||
<GroupMenu/>
|
||||
</div>
|
||||
</Collapse>
|
||||
<div className='w-full flex justify-between items-center p-4 bg-white'>
|
||||
{/* 左侧LOGO 标题 */}
|
||||
<div className='flex flex-none flex-grow-0'>
|
||||
<Logo/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='flex'>
|
||||
</div>
|
||||
|
||||
{/* 右侧功能 */}
|
||||
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
|
||||
<div onClick={toggleMenuOpen} className='w-18 cursor-pointer'>
|
||||
菜单 { isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
12
themes/Fukasawa/config_fuka.js
Normal file
12
themes/Fukasawa/config_fuka.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const FUKA_CONFIG = {
|
||||
|
||||
POST_LIST_COVER: true, // 文章列表显示图片封面
|
||||
|
||||
// 菜单
|
||||
MENU_ABOUT: true, // 显示关于
|
||||
MENU_CATEGORY: true, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: true, // 显示归档
|
||||
MENU_SEARCH: true // 显示搜索
|
||||
}
|
||||
export default FUKA_CONFIG
|
||||
10
themes/Fukasawa/index.js
Normal file
10
themes/Fukasawa/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export { LayoutIndex } from './LayoutIndex'
|
||||
export { LayoutSearch } from './LayoutSearch'
|
||||
export { LayoutArchive } from './LayoutArchive'
|
||||
export { LayoutSlug } from './LayoutSlug'
|
||||
export { Layout404 } from './Layout404'
|
||||
export { LayoutCategory } from './LayoutCategory'
|
||||
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
|
||||
export { LayoutPage } from './LayoutPage'
|
||||
export { LayoutTag } from './LayoutTag'
|
||||
export { LayoutTagIndex } from './LayoutTagIndex'
|
||||
35
themes/NEXT/Layout404.js
Normal file
35
themes/NEXT/Layout404.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import BLOG from '@/blog.config'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const Layout404 = () => {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
// 延时3秒如果加载失败就返回首页
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/').then(() => {
|
||||
console.log('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 30000000)
|
||||
})
|
||||
|
||||
return <LayoutBase meta={{ title: `${BLOG.title} | 页面找不到啦` }}>
|
||||
<div
|
||||
className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
|
||||
<div className='dark:text-gray-200'>
|
||||
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'><FontAwesomeIcon icon={faSpinner} spin={true} className='mr-2'/>404</h2>
|
||||
<div className='inline-block text-left h-32 leading-10 items-center'>
|
||||
<h2 className='m-0 p-0'>页面无法加载,即将返回首页</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
65
themes/NEXT/LayoutArchive.js
Normal file
65
themes/NEXT/LayoutArchive.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import React, { useEffect } from 'react'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import BlogPostArchive from './components/BlogPostArchive'
|
||||
import Live2D from './components/Live2D'
|
||||
|
||||
export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
// 深拷贝
|
||||
const postsSortByDate = Object.create(posts)
|
||||
|
||||
// 时间排序
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date.start_date.slice(0, 7)
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
archivePosts[date] = [post]
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<LayoutBase meta={meta} tags={tags} categories={categories} postCount={postCount}>
|
||||
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogPostArchive
|
||||
key={archiveTitle}
|
||||
posts={archivePosts[archiveTitle]}
|
||||
archiveTitle={archiveTitle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Live2D />
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +1,34 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import FloatDarkModeButton from '@/components/FloatDarkModeButton'
|
||||
import Footer from '@/components/Footer'
|
||||
import JumpToBottomButton from '@/components/JumpToBottomButton'
|
||||
import JumpToTopButton from '@/components/JumpToTopButton'
|
||||
import LoadingCover from '@/components/LoadingCover'
|
||||
import SideAreaLeft from '@/components/SideAreaLeft'
|
||||
import SideAreaRight from '@/components/SideAreaRight'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import FloatDarkModeButton from './components/FloatDarkModeButton'
|
||||
import Footer from './components/Footer'
|
||||
import JumpToBottomButton from './components/JumpToBottomButton'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
import LoadingCover from './components/LoadingCover'
|
||||
import SideAreaLeft from './components/SideAreaLeft'
|
||||
import SideAreaRight from './components/SideAreaRight'
|
||||
import TopNav from './components/TopNav'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import CONFIG_NEXT from './config_next'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param children
|
||||
* @param layout
|
||||
* @param fullWidth
|
||||
* @param tags
|
||||
* @param meta
|
||||
* @param post
|
||||
* @param totalPosts
|
||||
* @param currentSearch
|
||||
* @param currentCategory
|
||||
* @param currentTag
|
||||
* @param categories
|
||||
* @param customMeta
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BaseLayout = ({
|
||||
const LayoutBase = ({
|
||||
children,
|
||||
layout,
|
||||
fullWidth,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
@@ -45,8 +40,7 @@ const BaseLayout = ({
|
||||
currentSearch,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories,
|
||||
...customMeta
|
||||
categories
|
||||
}) => {
|
||||
const { onLoading } = useGlobal()
|
||||
const targetRef = useRef(null)
|
||||
@@ -81,11 +75,11 @@ const BaseLayout = ({
|
||||
|
||||
<>{headerSlot}</>
|
||||
|
||||
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block'></div>
|
||||
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block'/>
|
||||
|
||||
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
|
||||
<SideAreaLeft targetRef={targetRef} post={post} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
|
||||
<section id='center' className={`${BLOG.topNavType !== 'normal' ? 'mt-14' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
|
||||
<section id='center' className={`${CONFIG_NEXT.NAV_TYPE !== 'normal' ? 'mt-40' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
|
||||
{onLoading
|
||||
? <LoadingCover/>
|
||||
: <>
|
||||
@@ -111,8 +105,8 @@ const BaseLayout = ({
|
||||
)
|
||||
}
|
||||
|
||||
BaseLayout.propTypes = {
|
||||
LayoutBase.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
export default BaseLayout
|
||||
export default LayoutBase
|
||||
23
themes/NEXT/LayoutCategory.js
Normal file
23
themes/NEXT/LayoutCategory.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import StickyBar from './components/StickyBar'
|
||||
import CategoryList from './components/CategoryList'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
|
||||
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <LayoutBase meta={meta} tags={tags} currentCategory={category} postCount={postCount} latestPosts={latestPosts} categories={categories}>
|
||||
<StickyBar>
|
||||
<CategoryList currentCategory={category} categories={categories} />
|
||||
</StickyBar>
|
||||
<div className='md:mt-8'>
|
||||
<BlogPostListScroll posts={posts} tags={tags} currentCategory={category}/>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
38
themes/NEXT/LayoutCategoryIndex.js
Normal file
38
themes/NEXT/LayoutCategoryIndex.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faFolder, faThList } from '@fortawesome/free-solid-svg-icons'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const LayoutCategoryIndex = ({
|
||||
tags,
|
||||
allPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <LayoutBase meta={meta} totalPosts={allPosts} tags={tags} postCount={postCount} latestPosts={latestPosts}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'>
|
||||
<FontAwesomeIcon icon={faThList} className='mr-4' />{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{Object.keys(categories).map(category => {
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<div
|
||||
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})
|
||||
</div>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
29
themes/NEXT/LayoutIndex.js
Normal file
29
themes/NEXT/LayoutIndex.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
import Header from './components/Header'
|
||||
import LatestPostsGroup from './components/LatestPostsGroup'
|
||||
import Card from './components/Card'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import CONFIG_NEXT from './config_next'
|
||||
|
||||
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
return <LayoutBase
|
||||
headerSlot={CONFIG_NEXT.HOME_BANNER && <Header />}
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
|
||||
rightAreaSlot={
|
||||
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
{CONFIG_NEXT.POST_LIST_TYPE !== 'page'
|
||||
? (
|
||||
<BlogPostListScroll posts={posts} tags={tags} showSummary={true} />
|
||||
)
|
||||
: (
|
||||
<BlogPostListPage posts={posts} tags={tags} postCount={postCount} />
|
||||
)}
|
||||
</LayoutBase>
|
||||
}
|
||||
19
themes/NEXT/LayoutPage.js
Normal file
19
themes/NEXT/LayoutPage.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
import LatestPostsGroup from './components/LatestPostsGroup'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import CONFIG_NEXT from './config_next'
|
||||
|
||||
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
|
||||
return (
|
||||
<LayoutBase
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
|
||||
rightAreaSlot={CONFIG_NEXT.RIGHT_LATEST_POSTS && <LatestPostsGroup posts={latestPosts} />}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
56
themes/NEXT/LayoutSearch.js
Normal file
56
themes/NEXT/LayoutSearch.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
import StickyBar from './components/StickyBar'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const LayoutSearch = ({ posts, tags, categories, postCount }) => {
|
||||
let filteredPosts
|
||||
const searchKey = getSearchKey()
|
||||
if (searchKey) {
|
||||
filteredPosts = posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredPosts = posts
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.title} `,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return (
|
||||
<LayoutBase
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
postCount={postCount}
|
||||
currentSearch={searchKey}
|
||||
categories={categories}
|
||||
>
|
||||
<StickyBar>
|
||||
<div className="p-4 dark:text-gray-200">
|
||||
<FontAwesomeIcon icon={faSearch} className="mr-1" />{' '}
|
||||
{filteredPosts.length} {locale.COMMON.RESULT_OF_SEARCH}
|
||||
</div>
|
||||
</StickyBar>
|
||||
<div className="md:mt-5">
|
||||
<BlogPostListScroll posts={filteredPosts} tags={tags} showSummary={true}/>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
function getSearchKey () {
|
||||
const router = useRouter()
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
76
themes/NEXT/LayoutSlug.js
Normal file
76
themes/NEXT/LayoutSlug.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPageTableOfContents } from 'notion-utils'
|
||||
import TocDrawerButton from './components/TocDrawerButton'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import Card from './components/Card'
|
||||
import LatestPostsGroup from './components/LatestPostsGroup'
|
||||
import ArticleDetail from './components/ArticleDetail'
|
||||
import TocDrawer from './components/TocDrawer'
|
||||
import Live2D from './components/Live2D'
|
||||
import { useRef } from 'react'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import CONFIG_NEXT from './config_next'
|
||||
|
||||
export const LayoutSlug = ({
|
||||
post,
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.title}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
const drawerRight = useRef(null)
|
||||
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
|
||||
post.content = Object.keys(post?.blockMap?.block)
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
const floatSlot = post?.toc?.length > 1
|
||||
? <div className='block lg:hidden'><TocDrawerButton onClick={() => {
|
||||
drawerRight?.current?.handleSwitchVisible()
|
||||
}} /></div>
|
||||
: null
|
||||
|
||||
return (
|
||||
<LayoutBase
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
post={post}
|
||||
postCount={postCount}
|
||||
latestPosts={latestPosts}
|
||||
categories={categories}
|
||||
floatSlot={floatSlot}
|
||||
rightAreaSlot={
|
||||
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
>
|
||||
<ArticleDetail
|
||||
post={post}
|
||||
recommendPosts={recommendPosts}
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
|
||||
{/* 悬浮目录按钮 */}
|
||||
<div className='block lg:hidden'>
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
|
||||
{/* 宠物 */}
|
||||
<Live2D />
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
30
themes/NEXT/LayoutTag.js
Normal file
30
themes/NEXT/LayoutTag.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import StickyBar from './components/StickyBar'
|
||||
import TagList from './components/TagList'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
|
||||
export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts }) => {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const meta = {
|
||||
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
// 将当前选中的标签置顶🔝
|
||||
if (!tags) tags = []
|
||||
const currentTag = tags?.find(r => r?.name === tag)
|
||||
const newTags = currentTag ? [currentTag].concat(tags.filter(r => r?.name !== tag)) : tags.filter(r => r?.name !== tag)
|
||||
|
||||
return <LayoutBase meta={meta} tags={tags} currentTag={tag} categories={categories} postCount={postCount} latestPosts={latestPosts}>
|
||||
<StickyBar>
|
||||
<TagList tags={newTags} currentTag={tag}/>
|
||||
</StickyBar>
|
||||
<div className='md:mt-8'>
|
||||
<BlogPostListScroll posts={posts} tags={tags} currentTag={tag}/>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
25
themes/NEXT/LayoutTagIndex.js
Normal file
25
themes/NEXT/LayoutTagIndex.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import TagItem from './components/TagItem'
|
||||
|
||||
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <LayoutBase meta={meta} categories={categories} postCount={postCount} latestPosts={latestPosts}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTags} className='mr-4'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{ tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
export default function ArticleCopyright ({ author, url }) {
|
||||
if (!BLOG.widget?.showCopyRight) {
|
||||
if (!CONFIG_NEXT.ARTICLE_COPYRIGHT) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
@@ -11,7 +11,7 @@ export default function ArticleCopyright ({ author, url }) {
|
||||
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-700 bg-gray-100 p-5 leading-8 border-l-2 border-blue-500">
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
|
||||
<Link href="/about">
|
||||
<Link href={'/about'} >
|
||||
<a className="hover:underline">{author}</a>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -1,9 +1,9 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogAround from '@/components/BlogAround'
|
||||
import BlogAround from './BlogAround'
|
||||
import Comment from '@/components/Comment'
|
||||
import RecommendPosts from '@/components/RecommendPosts'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import TagItem from '@/components/TagItem'
|
||||
import RecommendPosts from './RecommendPosts'
|
||||
import ShareBar from './ShareBar'
|
||||
import TagItem from './TagItem'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -102,8 +102,7 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<FontAwesomeIcon icon={faEye} className='mr-1'/>
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv"
|
||||
></span>
|
||||
<span className="mr-2 busuanzi_value_page_pv"/>
|
||||
<span className='mr-2'>|</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +140,7 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138"></ins>
|
||||
data-ad-slot="3806269138"/>
|
||||
</section>
|
||||
|
||||
{/* 版权声明 */}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
@@ -9,11 +9,10 @@ import BLOG from '@/blog.config'
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
|
||||
const targetRef = useRef(null)
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return <div ref={targetRef}>
|
||||
return <div>
|
||||
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
@@ -8,10 +8,11 @@ import React from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import Card from './Card'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const showPreview = BLOG.home?.showPreview && post.blockMap
|
||||
const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
|
||||
@@ -67,7 +68,7 @@ const BlogPostCard = ({ post, showSummary }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{BLOG.home?.showPostCover && post?.page_cover && (
|
||||
{CONFIG_NEXT.POST_LIST_COVER && post?.page_cover && (
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<div className='h-72 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
|
||||
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
|
||||
@@ -1,7 +1,7 @@
|
||||
import BlogPostCard from '@/components/BlogPostCard'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import PaginationNumber from './PaginationNumber'
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListEmpty from '@/components/BlogPostListEmpty'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
@@ -1,19 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostCard from '@/components/BlogPostCard'
|
||||
import BlogPostListEmpty from '@/components/BlogPostListEmpty'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
/**
|
||||
* 博客列表滚动分页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @param targetRef 指向父容器,用于计算下拉滚动的高度
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home.showSummary }) => {
|
||||
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_NEXT.POST_LIST_SUMMARY }) => {
|
||||
const postsPerPage = BLOG.postsPerPage
|
||||
const [page, updatePage] = useState(1)
|
||||
const postsToShow = getPostByPage(page, posts, postsPerPage)
|
||||
38
themes/NEXT/components/Collapse.js
Normal file
38
themes/NEXT/components/Collapse.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
const Collapse = props => {
|
||||
const collapseRef = useRef(null)
|
||||
const collapseSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = sectionHeight + 'px'
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = 0 + 'px'
|
||||
})
|
||||
})
|
||||
}
|
||||
const expandSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
element.style.height = sectionHeight + 'px'
|
||||
const clearTime = setTimeout(() => {
|
||||
element.style.height = 'auto'
|
||||
}, 400)
|
||||
clearTimeout(clearTime)
|
||||
}
|
||||
useEffect(() => {
|
||||
const element = collapseRef.current
|
||||
if (props.isOpen) {
|
||||
expandSection(element)
|
||||
} else {
|
||||
collapseSection(element)
|
||||
}
|
||||
}, [props.isOpen])
|
||||
return (
|
||||
<div ref={collapseRef} style={{ height: '0px' }} className='overflow-hidden duration-200'>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Collapse.defaultProps = { isOpen: false }
|
||||
|
||||
export default Collapse
|
||||
@@ -1,6 +1,7 @@
|
||||
import { loadUserThemeFromCookies, saveTheme, useGlobal } from '@/lib/global'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
|
||||
const DarkModeButton = () => {
|
||||
const { changeTheme } = useGlobal()
|
||||
@@ -1,11 +1,11 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
export default function FloatDarkModeButton () {
|
||||
if (!BLOG.widget?.showDarkMode) {
|
||||
if (!CONFIG_NEXT.WIDGET_DARK_MODE) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Typed from 'typed.js'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
let wrapperTop = 0
|
||||
let windowTop = 0
|
||||
@@ -19,7 +19,7 @@ export default function Header () {
|
||||
if (!typed && window && document.getElementById('typed')) {
|
||||
changeType(
|
||||
new Typed('#typed', {
|
||||
strings: BLOG.home.homeBannerStrings,
|
||||
strings: CONFIG_NEXT.HOME_BANNER_Strings,
|
||||
typeSpeed: 200,
|
||||
backSpeed: 100,
|
||||
backDelay: 400,
|
||||
@@ -41,7 +41,7 @@ export default function Header () {
|
||||
const scrollTrigger = () => {
|
||||
if (
|
||||
(window.scrollY > windowTop) &
|
||||
(window.scrollY < window.innerHeight) &
|
||||
(window.scrollY < window.innerHeight) &&
|
||||
!autoScroll
|
||||
) {
|
||||
autoScroll = true
|
||||
@@ -50,7 +50,7 @@ export default function Header () {
|
||||
}
|
||||
if (
|
||||
(window.scrollY < windowTop) &
|
||||
(window.scrollY < window.innerHeight) &
|
||||
(window.scrollY < window.innerHeight) &&
|
||||
!autoScroll
|
||||
) {
|
||||
autoScroll = true
|
||||
@@ -99,7 +99,7 @@ export default function Header () {
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${BLOG.home.homeBannerImage}")`
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${CONFIG_NEXT.HOME_BANNER_IMAGE}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex h-full items-center lg:-mt-14 justify-center w-full text-4xl md:text-7xl text-white">
|
||||
@@ -1,8 +1,8 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
@@ -13,7 +13,7 @@ import smoothscroll from 'smoothscroll-polyfill'
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
if (!BLOG.widget?.showToBottom) {
|
||||
if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
@@ -13,7 +13,7 @@ import React from 'react'
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
if (!BLOG.widget?.showToTop) {
|
||||
if (!CONFIG_NEXT.WIDGET_TO_TOP) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import DarkModeButton from './DarkModeButton'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
import BLOG from '@/blog.config'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
let hasLoad = false
|
||||
export default function Live2D () {
|
||||
if (!BLOG.widget?.showPet) {
|
||||
if (!CONFIG_NEXT.WIDGET_PET) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function Live2D () {
|
||||
}
|
||||
|
||||
return <div className='fixed right-0 bottom-0 hidden md:block lg:mr-24 2xl:mr-40 z-20'>
|
||||
<canvas id="live2d"className='animate__slideInLeft animate__animated' width="280" height="250"></canvas>
|
||||
<canvas id="live2d" className='animate__slideInLeft animate__animated' width="280" height="250"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ function initLive2D () {
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then(() => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.widget.petLink)
|
||||
loadlive2d('live2d', CONFIG_NEXT.WIDGET_PET_LINK)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user