This commit is contained in:
tangly1024
2022-02-18 11:14:59 +08:00
42 changed files with 806 additions and 135 deletions

View File

@@ -46,6 +46,27 @@
**🕸  网址美观、搜索引擎优化**
- 更多特性、欢迎移步[我的博客](https://tangly1024.com/article/notion-next)查看
## 主题样式
目前提供了4种主题 ,只需修改`/themes/index.js`文件即可实现切换。
```javascript
// export * from './Empty' // 空模板
// export * from './NEXT'
// export * from './Fukasawa'
// export * from './Hexo'
export * from './Medium'
```
| 主题截图 | 预览地址 |
|--|--|
| <img src='./docs/theme-next.png' width='300'/> | [notion-next.tangly1024.com](https://notion-next.tangly1024.com) |
| <img src='./docs/theme-medium.png' width='300'/>| [notion-medium.tangly1024.com](https://notion-medium.tangly1024.com/) |
| <img src='./docs/theme-hexo.png' width='300'/> | [notion-hexo.tangly1024.com](http://notion-hexo.tangly1024.com/) |
| <img src='./docs/theme-fukasawa.png' width='300'/>| [notion-fukasawa.tangly1024.com](https://notion-fukasawa.tangly1024.com/) |
## 更新日志
请移步 [更新文档](https://docs.tangly1024.com/zh/changelog)查看
@@ -66,13 +87,9 @@
```bash
yarn # 安装依赖
yarn run dev # 本地开发
yarn run build # 本地打包编译
yarn run start # 本地启动NextJS服务
```
## 引用技术
@@ -83,9 +100,6 @@ yarn run start # 本地启动NextJS服务
- **评论**: Gitalk, Cusdis, Utterances
- **图标**[fontawesome](https://fontawesome.com/v5.15/icons?d=gallery)
## 页面样式主题
正在开发中..将支持配置文件切换主题
## License
The MIT License.

View File

@@ -11,7 +11,7 @@ const BLOG = {
LANG: 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
SINCE: 2021, // e.g if leave this empty, current year will be used.
BEI_AN: '', // 备案号 闽ICP备XXXXXXX
BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXXX
APPEARANCE: 'auto', // ['light', 'dark', 'auto'],
FONT: 'font-serif tracking-wider subpixel-antialiased', // 文章字体 ['font-sans', 'font-serif', 'font-mono'] @see https://www.tailwindcss.cn/docs/font-family
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc

View File

@@ -23,7 +23,7 @@ const Comment = ({ frontMatter }) => {
const router = useRouter()
const { locale } = useGlobal()
return (
<div className='comment mt-5 px-5 text-gray-800 dark:text-gray-300'>
<div className='comment mt-5 text-gray-800 dark:text-gray-300'>
<Tabs>
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
<ReactCusdis

View File

@@ -31,6 +31,7 @@ const CommonHead = ({ meta, children }) => {
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade"/> }
{meta?.type === 'article' && (
<>
<meta

View File

@@ -5,7 +5,7 @@ import React, { useState } from 'react'
* @param {*} param0
* @returns
*/
const Tabs = ({ children }) => {
const Tabs = ({ className, children }) => {
if (!children) {
return <></>
}
@@ -18,7 +18,7 @@ const Tabs = ({ children }) => {
})
if (count === 1) {
return <section className='duration-200'>
return <section className={'duration-200 ' + className}>
{children}
</section>
}
@@ -29,7 +29,7 @@ const Tabs = ({ children }) => {
setCurrentTab(i)
}
return <div className='mb-5 bg-white dark:bg-gray-800 duration-200'>
return <div className={'mb-5 bg-white dark:bg-gray-800 duration-200 ' + className}>
<ul className='flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600'>
{children.map((item, index) => {
return <li key={index}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 KiB

BIN
docs/theme-fukasawa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
docs/theme-hexo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/theme-medium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/theme-next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -28,6 +28,7 @@ export default function HeaderArticle ({ post }) {
}
}
useEffect(() => {
scrollTrigger()
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
@@ -36,6 +37,7 @@ export default function HeaderArticle ({ post }) {
return (
<div
id="header"
className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: headerImage }}
>

View File

@@ -27,7 +27,10 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) =>
const scrollTrigger = throttle(() => {
const scrollS = window.scrollY
const nav = document.querySelector('#sticky-nav')
if (scrollS >= windowTop && scrollS > 20) {
const header = document.querySelector('#header')
const showNav = (scrollS > 10 && scrollS >= windowTop) || (header && scrollS < 5) // 非首页无大图时影藏顶部 滚动条置顶时隐藏
if (!showNav) {
nav && nav.classList.replace('top-0', '-top-16')
windowTop = scrollS
} else {

View File

@@ -1,6 +1,7 @@
import LayoutBase from './LayoutBase'
export const Layout404 = () => {
return <div>
404 Not found.
</div>
export const Layout404 = (props) => {
return <LayoutBase {...props}>
<div className='w-full h-96 py-80 flex justify-center items-center'>404 Not found.</div>
</LayoutBase>
}

View File

@@ -2,7 +2,13 @@ import CommonHead from '@/components/CommonHead'
import React from 'react'
import Footer from './components/Footer'
import InfoCard from './components/InfoCard'
import LogoBar from './components/LogoBar'
import RevolverMaps from './components/RevolverMaps'
import CONFIG_MEDIUM from './config_medium'
import Tabs from '@/components/Tabs'
import TopNavBar from './components/TopNavBar'
import SearchInput from './components/SearchInput'
import BottomMenuBar from './components/BottomMenuBar'
import { useGlobal } from '@/lib/global'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -11,21 +17,41 @@ import LogoBar from './components/LogoBar'
* @constructor
*/
const LayoutBase = props => {
const { children, meta, showInfoCard = true } = props
const { children, meta, showInfoCard = true, slotRight, slotTop } = props
const { locale } = useGlobal()
return (
<div className='bg-white w-full h-full min-h-screen justify-center'>
<CommonHead meta={meta}/>
<main id="wrapper" className='max-w-7xl w-full h-full mx-auto'>
<LogoBar/>
<div className='pt-12 fixed top-24 w-80 pl-8 hidden lg:block'>
{showInfoCard && <InfoCard/>}
</div>
<div className='lg:ml-72 max-w-3xl w-full px-5'>
<div id='container' className='bg-white w-full h-full min-h-screen justify-center'>
<CommonHead meta={meta} />
<main id='wrapper' className='flex justify-between w-full h-full mx-auto'>
{/* 桌面端左侧菜单 */}
{/* <LeftMenuBar/> */}
<div className='w-full'>
{/* 移动端顶部菜单 */}
<TopNavBar />
<div className='px-5 max-w-5xl justify-center mx-auto'>
{slotTop}
{children}
</div>
</div>
{/* 桌面端右侧 */}
<div className='hidden xl:block border-l w-96'>
<Tabs className='py-14 px-6 sticky top-0'>
{slotRight}
<div key={locale.NAV.ABOUT}>
<SearchInput className='mt-6 mb-12' />
{showInfoCard && <InfoCard />}
{CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps />}
</div>
</Tabs>
</div>
</main>
<Footer/>
{/* 移动端底部 */}
<Footer />
<BottomMenuBar className='block md:hidden' />
</div>
)
}

View File

@@ -1,8 +1,13 @@
import LayoutBase from './LayoutBase'
import BlogPostListScroll from './components/BlogPostListScroll'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTh } from '@fortawesome/free-solid-svg-icons'
export const LayoutCategory = (props) => {
const { category } = props
return <LayoutBase {...props}>
Category - {category}
const slotTop = <div className='flex items-center font-sans p-8'><div className='text-xl'><FontAwesomeIcon icon={faTh} className='mr-2'/>分类</div>{category}</div>
return <LayoutBase {...props} slotTop={slotTop}>
<BlogPostListScroll {...props} />
</LayoutBase>
}

View File

@@ -2,7 +2,6 @@ import BlogPostListPage from './components/BlogPostListPage'
import LayoutBase from './LayoutBase'
export const LayoutIndex = (props) => {
// const { posts, tags, meta, categories, postCount, latestPosts } = props
return <LayoutBase {...props}>
<BlogPostListPage {...props}/>
</LayoutBase>

View File

@@ -2,8 +2,7 @@ import LayoutBase from './LayoutBase'
import BlogPostListPage from './components/BlogPostListPage'
export const LayoutPage = (props) => {
const { page, posts, postCount } = props
return <LayoutBase {...props}>
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
<BlogPostListPage {...props} />
</LayoutBase>
}

View File

@@ -1,31 +1,25 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
import BlogPostListPage from './components/BlogPostListPage'
import SearchInput from './components/SearchInput'
import { useGlobal } from '@/lib/global'
import TagGroups from './components/TagGroups'
import CategoryGroup from './components/CategoryGroup'
import BlogPostListScroll from './components/BlogPostListScroll'
export const LayoutSearch = (props) => {
const { posts } = props
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)
const { locale } = useGlobal()
return <LayoutBase {...props}>
Search {searchKey}
<div className='py-12'>
<div className='pb-4 w-full'>
{locale.NAV.SEARCH}
</div>
<SearchInput/>
<TagGroups {...props}/>
<CategoryGroup {...props}/>
</div>
<BlogPostListScroll {...props}/>
</LayoutBase>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

View File

@@ -6,20 +6,35 @@ import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import {
Code,
Collection,
CollectionRow,
Equation,
NotionRenderer
} from 'react-notion-x'
import LayoutBase from './LayoutBase'
import Comment from '@/components/Comment'
import Image from 'next/image'
import { useGlobal } from '@/lib/global'
import formatDate from '@/lib/formatDate'
import Link from 'next/link'
import mediumZoom from 'medium-zoom'
import React, { useEffect, useRef } from 'react'
import ArticleAround from './components/ArticleAround'
import Catalog from './components/Catalog'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faEye } from '@fortawesome/free-solid-svg-icons'
import CategoryItem from './components/CategoryItem'
import TagItemMini from './components/TagItemMini'
import CONFIG_MEDIUM from './config_medium'
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
export const LayoutSlug = (props) => {
const { post } = props
export const LayoutSlug = props => {
const { post, prev, next } = props
const meta = {
title: `${post.title} | ${BLOG.TITLE}`,
description: post.summary,
@@ -32,42 +47,124 @@ export const LayoutSlug = (props) => {
post.toc = getPageTableOfContents(post, post.blockMap)
}
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
const date = formatDate(
post?.date?.start_date || post.createdTime,
locale.LOCALE
)
return <LayoutBase {...props} meta={meta} showInfoCard={false}>
<h1 className='text-4xl mt-12 font-sans'>{post?.title}</h1>
<Link href='/about' passHref>
<div className='flex py-3 items-center font-sans cursor-pointer'>
<Image
alt={BLOG.AUTHOR}
width={25}
height={25}
loading='lazy'
src='/avatar.jpg'
className='rounded-full'
/>
<div className='mr-3 ml-1 text-green-500'>{BLOG.AUTHOR}</div>
<div className='text-gray-500'>{date}</div>
</div>
</Link>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1 max-w-5xl'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
</section>
<div>
<Comment frontMatter={post}/>
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('notion-article')
const imgList = container?.getElementsByTagName('img')
if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
zoomRef.current.attach(imgList[i])
}
}
})
const slotRight = post?.toc && post?.toc?.length > 3 && (
<div key={locale.COMMON.TABLE_OF_CONTENTS} className="mt-6">
<Catalog toc={post.toc} />
</div>
</LayoutBase>
)
return (
<LayoutBase
{...props}
meta={meta}
showInfoCard={true}
slotRight={slotRight}
>
<h1 className="text-4xl pt-12 font-sans">{post?.title}</h1>
<section className="flex py-4 items-center font-sans px-1">
<Link href="/about" passHref>
<>
<Image
alt={BLOG.AUTHOR}
width={25}
height={25}
loading="lazy"
src="/avatar.jpg"
className="rounded-full cursor-pointer"
/>
<div className="mr-3 ml-1 text-green-500 cursor-pointer">
{BLOG.AUTHOR}
</div>
</>
</Link>
<div className="text-gray-500">{date}</div>
<div className="hidden busuanzi_container_page_pv text-gray-500 font-light mr-2">
<FontAwesomeIcon icon={faEye} className="ml-3 mr-0.5" />
&nbsp;
<span className="mr-2 busuanzi_value_page_pv" />
</div>
</section>
{/* Notion文章主体 */}
<section id="notion-article" className="px-1 max-w-5xl">
{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>
<section>
<div className='flex justify-between'>
{ CONFIG_MEDIUM.POST_DETAIL_CATEGORY && <CategoryItem category={post.category}/>}
<div>
{ CONFIG_MEDIUM.POST_DETAIL_TAG && post?.tagItems?.map(tag => <TagItemMini key={tag.name} tag={tag} />)}
</div>
</div>
<ArticleAround prev={prev} next={next} />
<Comment frontMatter={post} />
</section>
</LayoutBase>
)
}
function getMediumZoomMargin () {
const width = window.innerWidth
if (width < 500) {
return 8
} else if (width < 800) {
return 20
} else if (width < 1280) {
return 30
} else if (width < 1600) {
return 40
} else if (width < 1920) {
return 48
} else {
return 72
}
}

View File

@@ -1,8 +1,13 @@
import LayoutBase from './LayoutBase'
import BlogPostListScroll from './components/BlogPostListScroll'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTag } from '@fortawesome/free-solid-svg-icons'
export const LayoutTag = (props) => {
const { tag } = props
return <LayoutBase>
Tag - {tag}
const slotTop = <div className='flex items-center font-sans p-8'><div className='text-xl'><FontAwesomeIcon icon={faTag} className='mr-2'/>标签</div>{tag}</div>
return <LayoutBase {...props} slotTop={slotTop}>
<BlogPostListScroll {...props} />
</LayoutBase>
}

View File

@@ -0,0 +1,26 @@
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleAround ({ prev, next }) {
if (!prev || !next) {
return <></>
}
return <section className='text-gray-800 h-12 flex items-center justify-between space-x-5 my-4'>
<Link href={`/article/${prev.slug}`} passHref>
<a className='text-sm cursor-pointer justify-start items-center flex hover:underline 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-end items-center flex hover:underline duration-300'>{next.title}
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
</a>
</Link>
</section>
}

View File

@@ -4,8 +4,10 @@ import { faAngleRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import { Code, Collection, Equation, NotionRenderer } from 'react-notion-x'
import CONFIG_MEDIUM from '../config_medium'
import CategoryItem from './CategoryItem'
import TagItemMini from './TagItemMini'
const BlogPostCard = ({ post, showSummary }) => {
const showPreview = CONFIG_MEDIUM.POST_LIST_PREVIEW && post.blockMap
@@ -15,13 +17,19 @@ const BlogPostCard = ({ post, showSummary }) => {
<div className='lg:p-8 p-4 flex flex-col w-full'>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={'cursor-pointer font-bold font-sans hover:underline text-4xl flex justify-start leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400'}>
<a className={'cursor-pointer font-bold font-sans hover:underline text-3xl flex justify-start leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400'}>
{post.title}
</a>
</Link>
<div className={'flex mt-2 items-center justify-start flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 '}>
{post.date.start_date}
<div className={'flex mt-2 items-center justify-start flex-wrap space-x-3 text-gray-400'}>
<div className='text-sm py-1'>{post.date.start_date}</div>
{ CONFIG_MEDIUM.POST_LIST_CATEGORY && <CategoryItem category={post.category}/>}
{ CONFIG_MEDIUM.POST_LIST_TAG && post?.tagItems?.map(tag => <TagItemMini key={tag.name} tag={tag} />)}
</div>
<div className='flex'>
</div>
{(!showPreview || showSummary) && <p className='my-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
@@ -36,7 +44,6 @@ const BlogPostCard = ({ post, showSummary }) => {
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>

View File

@@ -2,6 +2,7 @@ import BlogPostCard from './BlogPostCard'
import BLOG from '@/blog.config'
import BlogPostListEmpty from './BlogPostListEmpty'
import PaginationSimple from './PaginationSimple'
import { useRouter } from 'next/router'
/**
* 文章列表分页表格

View File

@@ -0,0 +1,106 @@
import BLOG from '@/blog.config'
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 { useRouter } from 'next/router'
/**
* 博客列表滚动分页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListScroll = ({ posts = [], currentSearch }) => {
const postsPerPage = BLOG.POSTS_PER_PAGE
const [page, updatePage] = useState(1)
let filteredPosts = Object.assign(posts)
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())
})
}
const postsToShow = getPostByPage(page, filteredPosts, postsPerPage)
let hasMore = false
if (filteredPosts) {
const totalCount = filteredPosts.length
hasMore = page * postsPerPage < totalCount
}
const handleGetMore = () => {
if (!hasMore) return
updatePage(page + 1)
}
// 监听滚动自动分页加载
const scrollTrigger = useCallback(throttle(() => {
const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
if (scrollS > clientHeight + 100) {
handleGetMore()
}
}, 500))
// 监听滚动
useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
})
const targetRef = useRef(null)
const { locale } = useGlobal()
if (!postsToShow || postsToShow.length === 0) {
return <BlogPostListEmpty currentSearch={currentSearch} />
} else {
return <div id='container' ref={targetRef} className='w-full'>
{/* 文章列表 */}
<div className='space-y-1 lg:space-y-4'>
{postsToShow.map(post => (
<BlogPostCard key={post.id} post={post} showSummary={true}/>
))}
</div>
<div>
<div onClick={() => {
handleGetMore()
}}
className='w-full my-4 py-4 text-center cursor-pointer glassmorphism shadow-xl rounded-xl dark:text-gray-200'
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`} </div>
</div>
</div>
}
}
/**
* 获取从第1页到指定页码的文章
* @param page 第几页
* @param totalPosts 所有文章
* @param postsPerPage 每页文章数量
* @returns {*}
*/
const getPostByPage = function (page, totalPosts, postsPerPage) {
return totalPosts.slice(
0,
postsPerPage * page
)
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}
export default BlogPostListScroll

View File

@@ -0,0 +1,27 @@
import { faHome, faSearch } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
import JumpToTopButton from '@/themes/Medium/components/JumpToTopButton'
export default function BottomMenuBar ({ className }) {
return (
<div className={'sticky bottom-0 w-full h-12 bg-white ' + className}>
<div className='flex justify-between h-full shadow-card'>
<Link href='/' passHref>
<div className='flex w-full items-center justify-center cursor-pointer'>
<FontAwesomeIcon icon={faHome} />
</div>
</Link>
<Link href='/search' passHref>
<div className='flex w-full items-center justify-center cursor-pointer'>
<FontAwesomeIcon icon={faSearch} />
</div>
</Link>
<div className='flex w-full items-center justify-center cursor-pointer'>
<JumpToTopButton/>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,85 @@
import React from 'react'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import Progress from './Progress'
import JumpToTopButton from './JumpToTopButton'
/**
* 目录导航组件
* @param toc
* @returns {JSX.Element}
* @constructor
*/
const Catalog = ({ toc }) => {
// 无目录就直接返回空
if (!toc || toc.length < 1) {
return <></>
}
// 监听滚动事件
React.useEffect(() => {
window.addEventListener('scroll', actionSectionScrollSpy)
actionSectionScrollSpy()
return () => {
window.removeEventListener('scroll', actionSectionScrollSpy)
}
}, [])
// 同步选中目录事件
const [activeSection, setActiveSection] = React.useState(null)
const throttleMs = 100
const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h')
let prevBBox = null
let currentSectionId = activeSection
for (let i = 0; i < sections.length; ++i) {
const section = sections[i]
if (!section || !(section instanceof Element)) continue
if (!currentSectionId) {
currentSectionId = section.getAttribute('data-id')
}
const bbox = section.getBoundingClientRect()
const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
const offset = Math.max(150, prevHeight / 4)
// GetBoundingClientRect returns values relative to viewport
if (bbox.top - offset < 0) {
currentSectionId = section.getAttribute('data-id')
prevBBox = bbox
continue
}
// No need to continue loop, if last element has been detected
break
}
setActiveSection(currentSectionId)
}, throttleMs))
return <div className='px-3'>
<div className='w-full py-1'>
<Progress/>
</div>
<nav className='font-sans overflow-y-auto scroll-hidden text-black'>
{toc.map((tocItem) => {
const id = uuidToId(tocItem.id)
return (
<a
key={id}
href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
>
<span
style={{
display: 'inline-block',
marginLeft: tocItem.indentLevel * 16
}}
className={`${activeSection === id && ' font-bold text-green-500 underline'}`}
>
{tocItem.text}
</span>
</a>
)
})}
</nav>
<JumpToTopButton className='text-gray-400 hover:bg-gray-100 py-1 duration-200'/>
</div>
}
export default Catalog

View File

@@ -0,0 +1,22 @@
import { faTh } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import CategoryItem from './CategoryItem'
const CategoryGroup = ({ currentCategory, categories }) => {
if (!categories) {
return <></>
}
return <div id='category-list' className='pt-4'>
<div className='mb-2'><FontAwesomeIcon icon={faTh} className='mr-2' />分类</div>
<div className='flex flex-wrap'>
{Object.keys(categories).map(category => {
const selected = currentCategory === category
const categoryCount = +categories[category]
return <CategoryItem key={category} selected={selected} category={category} categoryCount={categoryCount}/>
})}
</div>
</div>
}
export default CategoryGroup

View File

@@ -0,0 +1,16 @@
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolderOpen, faFolder } from '@fortawesome/free-solid-svg-icons'
export default function CategoryItem ({ selected, category, categoryCount }) {
return <Link 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 dark:hover:text-white hover:bg-gray-600') +
' flex text-sm items-center duration-300 cursor-pointer py-1 font-light px-2 whitespace-nowrap'}>
<div><FontAwesomeIcon icon={selected ? faFolderOpen : faFolder}
className={'mr-2'} />{category} {categoryCount && (categoryCount)}
</div>
</a>
</Link>
}

View File

@@ -5,9 +5,9 @@ import React from 'react'
import SocialButton from './SocialButton'
const InfoCard = () => {
return <>
<div className='items-center justify-start font-sans '>
<div className='hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/about') }}>
return <div id='info-card' className='py-4'>
<div className='items-center justify-center font-sans '>
<div className='hover:scale-105 transform duration-200 cursor-pointer flex justify-center' onClick={ () => { Router.push('/about') }}>
<Image
alt={BLOG.AUTHOR}
width={120}
@@ -17,11 +17,11 @@ const InfoCard = () => {
className='rounded-full'
/>
</div>
<div className='text-xl py-2 hover:scale-105 transform duration-200'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200'>{BLOG.BIO}</div>
<div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.BIO}</div>
<SocialButton/>
</div>
</>
</div>
}
export default InfoCard

View File

@@ -0,0 +1,28 @@
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_MEDIUM from '../config_medium'
/**
* 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件
* @param targetRef 关联高度的目标html标签
* @param showPercent 是否显示百分比
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = ({ showPercent = false, percent, className }) => {
if (!CONFIG_MEDIUM.WIDGET_TO_TOP) {
return <></>
}
const { locale } = useGlobal()
return (<div className={'flex space-x-1 items-center cursor-pointer w-full justify-center ' + className } onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
<div title={locale.POST.TOP} >
<FontAwesomeIcon icon={faArrowUp} />
</div>
{showPercent && (<div className='text-xs dark:text-gray-200 block lg:hidden'>{percent}%</div>)}
</div>)
}
export default JumpToTopButton

View File

@@ -0,0 +1,16 @@
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import React from 'react'
export default function LeftMenuBar () {
return <div className='w-20 border-r hidden lg:block pt-12'>
<section>
<Link href='/'>
<div className='text-center cursor-pointer hover:text-black'>
<FontAwesomeIcon icon={faHome} size='lg' color='gray' />
</div>
</Link>
</section>
</div>
}

View File

@@ -1,25 +1,14 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
export default function LogoBar () {
const { locale } = useGlobal()
return <div id='top-wrapper' className='w-full flex justify-center font-sans'>
<div className='flex mx-auto w-full items-center space-x-3 py-6 px-5'>
<Link href='/'>
<a className='text-3xl'>{BLOG.TITLE}</a>
</Link>
<Link href='/about'>
<a className='text-gray-600'>{locale.NAV.ABOUT}</a>
</Link>
{BLOG.CONTACT_EMAIL && <Link href={`mailto:${BLOG.CONTACT_EMAIL}`} passHref>
<div className='bg-black px-2 py-1 rounded-full'>
<FontAwesomeIcon className='cursor-pointer text-white' icon={faEnvelope}/>
</div>
</Link>}
</div>
<div className='flex mx-auto w-full justify-between '>
<div className='space-x-3 flex items-center'>
<Link href='/'>
<a className='text-2xl'>{BLOG.TITLE}</a>
</Link>
</div>
</div>
</div>
}

View File

@@ -6,14 +6,15 @@ import { useGlobal } from '@/lib/global'
/**
* 简易翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @param totalPage 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const PaginationSimple = ({ page, showNext }) => {
const PaginationSimple = ({ page, totalPage }) => {
const { locale } = useGlobal()
const router = useRouter()
const currentPage = +page
const showNext = currentPage <= totalPage
return (
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
<Link

View File

@@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react'
/**
* 顶部页面阅读进度条
* @returns {JSX.Element}
* @constructor
*/
const Progress = ({ targetRef, showPercent = true }) => {
const currentRef = targetRef?.current || targetRef
const [percent, changePercent] = useState(0)
const scrollListener = () => {
const target = currentRef || document.getElementById('container')
if (target) {
const clientHeight = target.clientHeight
const scrollY = window.pageYOffset
const fullHeight = clientHeight - window.outerHeight
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
if (per > 100) per = 100
if (per < 0) per = 0
changePercent(per)
}
}
useEffect(() => {
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [percent])
return (
<div className="h-4 w-full shadow-2xl bg-gray-400 font-sans">
<div
className="h-4 bg-gray-600 duration-200"
style={{ width: `${percent}%` }}
>
{showPercent && (
<div className="text-right text-white text-xs">{percent}%</div>
)}
</div>
</div>
)
}
export default Progress

View File

@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react'
export default function RevolverMaps () {
const [load, changeLoad] = useState(false)
useEffect(() => {
if (!load) {
initRevolverMaps()
changeLoad(true)
}
})
return <div id="revolvermaps" className='p-4'/>
}
function initRevolverMaps () {
if (screen.width >= 768) {
Promise.all([
loadExternalResource('https://rf.revolvermaps.com/0/0/8.js?i=5jnp1havmh9&amp;m=0&amp;c=ff0000&amp;cr1=ffffff&amp;f=arial&amp;l=33')
]).then(() => {
console.log('地图加载完成')
})
}
}
// 封装异步加载资源的方法
function loadExternalResource (url) {
return new Promise((resolve, reject) => {
const container = document.getElementById('revolvermaps')
const tag = document.createElement('script')
tag.src = url
if (tag) {
tag.onload = () => resolve(url)
tag.onerror = () => reject(url)
container.appendChild(tag)
}
})
}

View File

@@ -0,0 +1,77 @@
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, className }) => {
const [searchKey, setSearchKey] = useState(currentSearch || getSearchKey() || '')
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 ' + className}>
<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>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}
export default SearchInput

View File

@@ -10,8 +10,7 @@ import React from 'react'
* @constructor
*/
const SocialButton = () => {
return <div className='w-52 flex-wrap flex'>
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 '>
return <div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 flex-wrap flex justify-center '>
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
<FontAwesomeIcon icon={faGithub} className='transform hover:scale-125 duration-150'/>
</a>}
@@ -31,6 +30,5 @@ const SocialButton = () => {
<FontAwesomeIcon icon={faRss} className='transform hover:scale-125 duration-150'/>
</a>
</div>
</div>
}
export default SocialButton

View File

@@ -0,0 +1,29 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import TagItemMini from './TagItemMini'
/**
* 标签组
* @param tags
* @param currentTag
* @returns {JSX.Element}
* @constructor
*/
const TagGroups = ({ tags, currentTag }) => {
if (!tags) return <></>
return (
<div id='tags-group' className='dark:border-gray-600 py-4'>
<div className='mb-2'><FontAwesomeIcon icon={faTag} className='mr-2' />标签</div>
<div className='space-y-2'>
{
tags.map(tag => {
const selected = tag.name === currentTag
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
})
}
</div>
</div>
)
}
export default TagGroups

View File

@@ -5,7 +5,7 @@ import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => {
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white
${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-gray-900'
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}` }>

View File

@@ -0,0 +1,9 @@
import LogoBar from '@/themes/Medium/components/LogoBar'
export default function TopNavBar ({ className }) {
return <div id='top-nav' className={'sticky top-0 lg:relative w-full z-50 ' + className}>
<div className='flex w-full h-12 shadow bg-white px-5 items-center'>
<LogoBar />
</div>
</div>
}

View File

@@ -1,13 +1,22 @@
const CONFIG_MEDIUM = {
POST_LIST_COVER: true, // 文章列表显示图片封面
POST_LIST_PREVIEW: true, // 显示文章预览
POST_LIST_PREVIEW: true, // 列表显示文章预览
POST_LIST_CATEGORY: true, // 列表显示文章分类
POST_LIST_TAG: true, // 列表显示文章标签
POST_DETAIL_CATEGORY: true, // 文章显示分类
POST_DETAIL_TAG: true, // 文章显示标签
// 菜单
MENU_ABOUT: true, // 显示关于
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索
MENU_SEARCH: true, // 显示搜索
// Widget
WIDGET_REVOLVER_MAPS: process.env.NEXT_PUBLIC_WIDGET_REVOLVER_MAPS || 'false', // 地图插件
WIDGET_TO_TOP: true // 跳回顶部
}
export default CONFIG_MEDIUM