mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
贡献代码相关
This commit is contained in:
20
README.md
20
README.md
@@ -50,12 +50,9 @@
|
||||
|--|--|--|--|
|
||||
| <img src='./docs/theme-next.png' width='300'/> [预览NEXT](https://preview.tangly1024.com/?theme=next) | <img src='./docs/theme-medium.png' width='300'/> [预览MEDIUM](https://preview.tangly1024.com/?theme=medium) | <img src='./docs/theme-hexo.png' width='300'/> [预览HEXO](https://preview.tangly1024.com/?theme=hexo) | <img src='./docs/theme-fukasawa.png' width='300'/> [预览FUKASAWA](https://preview.tangly1024.com/?theme=fukasawa) |
|
||||
|
||||
*只需修改`blog.config.js`文件的`THEME`即可实现主题切换。* 没找到喜欢的主题?[贡献](/CONTRIBUTING.md)一个吧~
|
||||
|
||||
|
||||
*只需修改`blog.config.js`文件的`THEME`即可实现主题切换。*
|
||||
|
||||
## 更新日志
|
||||
请移步 [更新文档](https://docs.tangly1024.com/zh/changelog)查看
|
||||
|
||||
## 快速起步
|
||||
|
||||
@@ -80,11 +77,15 @@ yarn run start # 本地启动NextJS服务
|
||||
|
||||
## 引用技术
|
||||
|
||||
- **框架**: Next.js
|
||||
- **框架**: [Next.js](https://nextjs.org)
|
||||
- **样式**: [Tailwind CSS](https://www.tailwindcss.cn/) 和 `@tailwindcss/jit` compiler
|
||||
- **渲染**: [React-notion-x](https://github.com/NotionX/react-notion-x)
|
||||
- **评论**: Gitalk, Cusdis, Utterances
|
||||
- **图标**:[fontawesome](https://fontawesome.com/v5.15/icons?d=gallery)
|
||||
- **评论**: [Giscus](https://giscus.app/zh-CN), [Gitalk](https://gitalk.github.io), [Cusdis](https://gitalk.github.io), [Utterances](https://utteranc.es)
|
||||
- **图标**:[fontawesome v5.15](https://fontawesome.com/v5.15/icons?d=gallery)
|
||||
|
||||
## 更新日志
|
||||
请移步 [更新文档](https://docs.tangly1024.com/zh/changelog)查看
|
||||
|
||||
|
||||
## 致谢
|
||||
感谢Craig Hart发起的Nobelium项目
|
||||
@@ -92,13 +93,16 @@ yarn run start # 本地启动NextJS服务
|
||||
<td align="center"><a href="https://github.com/craigary" title="Craig Hart"><img src="https://avatars.githubusercontent.com/u/10571717" width="64px;"alt="Craig Hart"/></a><br/><a href="https://notion.so/cnotion" title="Craig Hart">Craig Hart</a></td>
|
||||
</tr></table>
|
||||
|
||||
## Contributors
|
||||
## 贡献者
|
||||
|
||||
<table><tr align="left">
|
||||
<td align="center"><a href="https://github.com/tangly1024"><img src="https://avatars.githubusercontent.com/u/15920488" width="64px;"alt="tangly1024"/><br/><sub><b>tangly1024</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=tangly1024" title="Owner" >🎫 🔧 🎨 🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/uWayLu"><img src="https://avatars.githubusercontent.com/u/21689326" width="64px;" alt="uWayLu"/><br/><sub><b>uWayLu</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=uWayLu" title="yokinist" >🔧 🐛</a></td>
|
||||
</tr></table>
|
||||
|
||||
十分期待你的[贡献](/CONTRIBUTING.md),一起来完善这个项目~
|
||||
|
||||
|
||||
## License
|
||||
|
||||
The MIT License.
|
||||
|
||||
@@ -2,7 +2,7 @@ import BLOG from '@/blog.config'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useState } from 'react'
|
||||
import Select from './Select'
|
||||
import { ALL_THEME } from '@/lib/theme'
|
||||
import { ALL_THEME } from '@/themes'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -2,7 +2,8 @@ import { generateLocaleDict, initLocale } from './lang'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import Router from 'next/router'
|
||||
import BLOG from '@/blog.config'
|
||||
import { ALL_THEME, initDarkMode, initTheme, saveThemeToCookies } from '@/lib/theme'
|
||||
import { initDarkMode, initTheme, saveThemeToCookies } from '@/lib/theme'
|
||||
import { ALL_THEME } from '@/themes'
|
||||
|
||||
const GlobalContext = createContext()
|
||||
let hasInit = false
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import cookie from 'react-cookies'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'empty']
|
||||
/**
|
||||
* 初始化主题
|
||||
* @param isDarkMode
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
export const Layout404 = () => {
|
||||
return <div>
|
||||
404 Not found.
|
||||
</div>
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutArchive = props => {
|
||||
const { posts } = props
|
||||
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]
|
||||
}
|
||||
})
|
||||
return (
|
||||
<LayoutBase {...props} meta={meta}>
|
||||
<div className="mb-10 pb-20 md:p-12 p-3 min-h-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<div key={archiveTitle}>
|
||||
<div
|
||||
className="pt-16 pb-4 text-3xl dark:text-gray-300"
|
||||
id={archiveTitle}
|
||||
>
|
||||
{archiveTitle}
|
||||
</div>
|
||||
<ul>
|
||||
{archivePosts[archiveTitle].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 id={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>
|
||||
))}
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, meta, customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
|
||||
|
||||
let links = [
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommonHead meta={meta} />
|
||||
{/* 导航菜单 */}
|
||||
<div className="w-full flex justify-center my-2">
|
||||
<div className=" max-w-6xl justify-between w-full flex">
|
||||
<section>
|
||||
<Link title={BLOG.TITLE} href={'/'}>
|
||||
<a className={'cursor-pointer flex items-center hover:underline'}>
|
||||
<i className={'fas fa-home mr-1'} />
|
||||
<div className="text-center">{BLOG.TITLE} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
<nav className="space-x-3 flex">
|
||||
{links.map(link => {
|
||||
if (link) {
|
||||
return (
|
||||
<Link key={`${link.to}`} title={link.to} href={link.to}>
|
||||
<a
|
||||
className={
|
||||
'cursor-pointer flex items-center hover:underline'
|
||||
}
|
||||
>
|
||||
<i className={`${link.icon} mr-1`} />
|
||||
<div className="text-center">{link.name}</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容主体 */}
|
||||
<main id="wrapper" className="flex justify-center flex-1 pb-12">
|
||||
<div className="max-w-4xl w-full px-3">{children}</div>
|
||||
<div className='hidden md:block'>
|
||||
<div className="sticky top-0 z-40">
|
||||
<Live2D />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer className="font-sans dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-sm p-6">
|
||||
<i className="fas fa-copyright" /> {`${startYear}${currentYear}`}{' '}
|
||||
<span>
|
||||
<i className="mx-1 animate-pulse fas fa-heart" />{' '}
|
||||
<a
|
||||
href={BLOG.LINK}
|
||||
className="underline font-bold dark:text-gray-300 "
|
||||
>
|
||||
{BLOG.AUTHOR}
|
||||
</a>
|
||||
.
|
||||
<br />
|
||||
<span>
|
||||
Powered by{' '}
|
||||
<a
|
||||
href="https://notion.so"
|
||||
className="underline font-bold dark:text-gray-300"
|
||||
>
|
||||
Notion
|
||||
</a>{' '}
|
||||
&{' '}
|
||||
<a
|
||||
href="https://github.com/tangly1024/NotionNext"
|
||||
className="underline font-bold dark:text-gray-300"
|
||||
>
|
||||
NotionNext {BLOG.VERSION}
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</span>
|
||||
{BLOG.BEI_AN && (
|
||||
<>
|
||||
<br />
|
||||
<i className="fas fa-shield-alt" />{' '}
|
||||
<a href="https://beian.miit.gov.cn/" className="mr-2">
|
||||
{BLOG.BEI_AN}
|
||||
</a>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<span className="hidden busuanzi_container_site_pv">
|
||||
<i className="fas fa-eye" />
|
||||
<span className="px-1 busuanzi_value_site_pv"> </span>{' '}
|
||||
</span>
|
||||
<span className="pl-2 hidden busuanzi_container_site_uv">
|
||||
<i className="fas fa-users" />{' '}
|
||||
<span className="px-1 busuanzi_value_site_uv"> </span>{' '}
|
||||
</span>
|
||||
<br />
|
||||
<h1>{meta?.title || BLOG.TITLE}</h1>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
@@ -1,48 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategory = props => {
|
||||
const { category, posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
let hasMore = false
|
||||
const postsToShow = posts
|
||||
? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
|
||||
: []
|
||||
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
|
||||
}
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
Category - {category}
|
||||
{postsToShow.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategoryIndex = (props) => {
|
||||
const { categories } = props
|
||||
const { locale } = useGlobal()
|
||||
return <LayoutBase {...props}>
|
||||
<div className=' p-10'>
|
||||
<div className='dark:text-gray-200 mb-5'>
|
||||
<i className='mr-4 fas fa-th' />{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categories && categories.map(category => {
|
||||
return <Link key={category.name} href={`/category/${category.name}`} 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'}>
|
||||
<i className='mr-4 fas fa-folder' />{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutIndex = props => {
|
||||
const { posts, postCount } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
const page = 1
|
||||
const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount
|
||||
|
||||
const currentPage = +page
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
{posts.map(p => (
|
||||
<div key={p.id} className='border my-12'>
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className='underline cursor-pointer'>{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
|
||||
<Link
|
||||
href={ {
|
||||
pathname: (currentPage === 2 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
|
||||
} } passHref >
|
||||
<a
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'visible'} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
||||
<a
|
||||
rel='next'
|
||||
className={`${showNext ? 'visible' : 'invisible'} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutPage = (props) => {
|
||||
const { page } = props
|
||||
const { posts, postCount } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount
|
||||
|
||||
const currentPage = +page
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
{posts.map(p => (
|
||||
<div key={p.id} className='border my-12'>
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className='underline cursor-pointer'>{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
|
||||
<Link
|
||||
href={ {
|
||||
pathname: (currentPage === 2 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
|
||||
} } passHref >
|
||||
<a
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'visible'} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
||||
<a
|
||||
rel='next'
|
||||
className={`${showNext ? 'visible' : 'invisible'} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SearchInput from './components/SearchInput'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSearch = props => {
|
||||
const { keyword, posts } = props
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('container')
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${keyword}</span>`)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
let hasMore = false
|
||||
const postsToShow = posts
|
||||
? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
|
||||
: []
|
||||
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
|
||||
}
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<h2>Search - {keyword}</h2>
|
||||
<SearchInput {...props} />
|
||||
{postsToShow.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { getPageTableOfContents } from 'notion-utils'
|
||||
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 {
|
||||
Code,
|
||||
Collection,
|
||||
CollectionRow,
|
||||
Equation,
|
||||
NotionRenderer
|
||||
} from 'react-notion-x'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
export const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.TITLE}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
if (!lock && post?.blockMap?.block) {
|
||||
post.content = Object.keys(post.blockMap.block)
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
})
|
||||
return (
|
||||
<LayoutBase {...props} meta={meta}>
|
||||
<div>
|
||||
<h2>{post?.title}</h2>
|
||||
|
||||
{lock && <ArticleLock password={post.password} validPassword={validPassword} />}
|
||||
|
||||
{!lock && <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>}
|
||||
|
||||
</div>
|
||||
</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,49 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTag = props => {
|
||||
const { tag, posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
|
||||
let hasMore = false
|
||||
const postsToShow = posts
|
||||
? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
|
||||
: []
|
||||
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
|
||||
}
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase>
|
||||
Tag - {tag}
|
||||
{postsToShow.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTagIndex = (props) => {
|
||||
const { tags } = props
|
||||
const { locale } = useGlobal()
|
||||
return <LayoutBase {...props}>
|
||||
<div className='p-10'>
|
||||
<div className='dark:text-gray-200 mb-5'><i className='mr-4 fas fa-tag'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{ tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'>
|
||||
<Link key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
|
||||
mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
|
||||
<div className='font-light dark:text-gray-400'><i className='mr-1 fas fa-tag'/> {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
}) }
|
||||
</div>
|
||||
</div> </LayoutBase>
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleLock = props => {
|
||||
const { password, validPassword } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const submitPassword = () => {
|
||||
const p = document.getElementById('password')
|
||||
if (p && p.value && p.value === password) {
|
||||
validPassword(true)
|
||||
} else {
|
||||
const tips = document.getElementById('tips')
|
||||
if (tips) {
|
||||
tips.innerHTML = ''
|
||||
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lgfont-light leading-10 text-black bg-white dark:bg-gray-500'></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
|
||||
let lock = false
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef(null)
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleSearch = () => {
|
||||
const key = searchInputRef.current.value
|
||||
if (key && key !== '') {
|
||||
router.push({ pathname: '/search/' + key }).then(r => {
|
||||
console.log('搜索', key)
|
||||
})
|
||||
} 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 = ''
|
||||
setShowClean(false)
|
||||
}
|
||||
function lockSearchInput () {
|
||||
lock = true
|
||||
}
|
||||
|
||||
function unLockSearchInput () {
|
||||
lock = false
|
||||
}
|
||||
const [showClean, setShowClean] = useState(false)
|
||||
const updateSearchKey = (val) => {
|
||||
if (lock) {
|
||||
return
|
||||
}
|
||||
searchInputRef.current.value = val
|
||||
if (val) {
|
||||
setShowClean(true)
|
||||
} else {
|
||||
setShowClean(false)
|
||||
}
|
||||
}
|
||||
|
||||
return <section className='flex w-full bg-gray-100'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
onCompositionEnd={unLockSearchInput}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={currentSearch || ''}
|
||||
/>
|
||||
|
||||
<div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
|
||||
onClick={handleSearch}>
|
||||
<i className={'hover:text-black transform duration-200 text-gray-500 cursor-pointer fas fa-search'} />
|
||||
</div>
|
||||
|
||||
{(showClean &&
|
||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||
<i className='hover:text-black transform duration-200 text-gray-400 cursor-pointer fas fa-times' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
@@ -1,4 +0,0 @@
|
||||
const CONFIG_EMPTY = {
|
||||
TEST_CONFIG: 'TESET'
|
||||
}
|
||||
export default CONFIG_EMPTY
|
||||
@@ -1,25 +0,0 @@
|
||||
import CONFIG_EMPTY from './config_empty'
|
||||
import { LayoutIndex } from './LayoutIndex'
|
||||
import { LayoutSearch } from './LayoutSearch'
|
||||
import { LayoutArchive } from './LayoutArchive'
|
||||
import { LayoutSlug } from './LayoutSlug'
|
||||
import { Layout404 } from './Layout404'
|
||||
import { LayoutCategory } from './LayoutCategory'
|
||||
import { LayoutCategoryIndex } from './LayoutCategoryIndex'
|
||||
import { LayoutPage } from './LayoutPage'
|
||||
import { LayoutTag } from './LayoutTag'
|
||||
import { LayoutTagIndex } from './LayoutTagIndex'
|
||||
|
||||
export {
|
||||
CONFIG_EMPTY as THEME_CONFIG,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
LayoutSlug,
|
||||
Layout404,
|
||||
LayoutCategory,
|
||||
LayoutCategoryIndex,
|
||||
LayoutPage,
|
||||
LayoutTag,
|
||||
LayoutTagIndex
|
||||
}
|
||||
@@ -5,11 +5,7 @@ import * as next from './next'
|
||||
import * as fukasawa from './fukasawa'
|
||||
import * as hexo from './hexo'
|
||||
import * as medium from './medium'
|
||||
import * as empty from './empty'
|
||||
export {
|
||||
next,
|
||||
fukasawa,
|
||||
hexo,
|
||||
medium,
|
||||
empty
|
||||
}
|
||||
import * as example from './example'
|
||||
|
||||
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'example']
|
||||
export { hexo, next, medium, fukasawa, example }
|
||||
|
||||
Reference in New Issue
Block a user