mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
合并
This commit is contained in:
28
README.md
28
README.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
docs/theme-fukasawa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/theme-hexo.png
Normal file
BIN
docs/theme-hexo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/theme-medium.png
Normal file
BIN
docs/theme-medium.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
docs/theme-next.png
Normal file
BIN
docs/theme-next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -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 }}
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
<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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
26
themes/Medium/components/ArticleAround.js
Normal file
26
themes/Medium/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-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>
|
||||
}
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
|
||||
106
themes/Medium/components/BlogPostListScroll.js
Normal file
106
themes/Medium/components/BlogPostListScroll.js
Normal 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
|
||||
27
themes/Medium/components/BottomMenuBar.js
Normal file
27
themes/Medium/components/BottomMenuBar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
85
themes/Medium/components/Catalog.js
Normal file
85
themes/Medium/components/Catalog.js
Normal 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
|
||||
22
themes/Medium/components/CategoryGroup.js
Normal file
22
themes/Medium/components/CategoryGroup.js
Normal 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
|
||||
16
themes/Medium/components/CategoryItem.js
Normal file
16
themes/Medium/components/CategoryItem.js
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
28
themes/Medium/components/JumpToTopButton.js
Normal file
28
themes/Medium/components/JumpToTopButton.js
Normal 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
|
||||
16
themes/Medium/components/LeftMenuBar.js
Normal file
16
themes/Medium/components/LeftMenuBar.js
Normal 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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
43
themes/Medium/components/Progress.js
Normal file
43
themes/Medium/components/Progress.js
Normal 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
|
||||
36
themes/Medium/components/RevolverMaps.js
Normal file
36
themes/Medium/components/RevolverMaps.js
Normal 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&m=0&c=ff0000&cr1=ffffff&f=arial&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)
|
||||
}
|
||||
})
|
||||
}
|
||||
77
themes/Medium/components/SearchInput.js
Normal file
77
themes/Medium/components/SearchInput.js
Normal 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
|
||||
@@ -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
|
||||
|
||||
29
themes/Medium/components/TagGroups.js
Normal file
29
themes/Medium/components/TagGroups.js
Normal 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
|
||||
@@ -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`}` }>
|
||||
|
||||
9
themes/Medium/components/TopNavBar.js
Normal file
9
themes/Medium/components/TopNavBar.js
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user