theme-simple & menun

This commit is contained in:
tangly1024.com
2023-03-08 20:10:23 +08:00
parent b22475dec5
commit 556c518ea7
50 changed files with 1088 additions and 37 deletions

View File

@@ -10,6 +10,8 @@ const BLOG = {
SINCE: 2021, // e.g if leave this empty, current year will be used.
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 dark夜间模式 auto根据时间和主题自动夜间模式
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型从3.12.0版本起各主题将逐步支持灵活的二级菜单配置替代了原来的Page类型此配置是试验功能、默认关闭。
AUTHOR: process.env.NEXT_PUBLIC_AUTHOR || 'NotionNext', // 您的昵称 例如 tangly1024
BIO: process.env.NEXT_PUBLIC_BIO || '一个普通的干饭人🍚', // 作者简介
LINK: process.env.NEXT_PUBLIC_LINK || 'https://tangly1024.com', // 网站地址
@@ -198,6 +200,8 @@ const BLOG = {
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时为博文。
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时为单页。
type_notice: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时为公告。
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时为菜单。
type_sub_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时为子菜单。
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
status_publish: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文

View File

@@ -85,18 +85,39 @@ function getCustomNav({ allPages }) {
const customNav = []
if (allPages && allPages.length > 0) {
allPages.forEach(p => {
if (p?.status === 'Published' && p?.type === 'Page') {
if (p?.slug?.indexOf('http') === 0) {
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
} else {
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
}
if (p?.slug?.indexOf('http') === 0) {
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
} else {
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
}
})
}
return customNav
}
function getCustomMenu({ collectionData }) {
const menuPages = collectionData.filter(post => (post.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published')
const menus = []
if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => {
e.show = true
if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) {
menus.push(e)
} else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
const parentMenu = menus[menus.length - 1]
if (parentMenu) {
if (parentMenu.subMenus) {
parentMenu.subMenus.push(e)
} else {
parentMenu.subMenus = [e]
}
}
}
})
}
return menus
}
/**
* 获取标签选项
* @param schema
@@ -219,6 +240,8 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
const siteInfo = getBlogInfo({ collection, block })
const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') })
// 新的菜单
const customMenu = getCustomMenu({ collectionData })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
return {
@@ -236,6 +259,7 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
categoryOptions,
rawMetadata,
customNav,
customMenu,
postCount,
pageIds,
latestPosts

View File

@@ -76,10 +76,14 @@ export default async function getPageProperties(id, block, schema, authToken, ta
// 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识
mapProperties(properties)
if (properties.type === 'Post') {
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
properties.slug = BLOG.POST_URL_PREFIX ? (BLOG.POST_URL_PREFIX + '/' + (properties.slug ?? properties.id)) : (properties.slug ?? properties.id)
} else {
properties.slug = (properties.slug ?? properties.id)
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
properties.slug = properties.slug ?? properties.id
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_menu || properties.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
// 菜单路径为空、作为可展开菜单使用
properties.to = properties.slug ?? '#'
properties.name = properties.title ?? ''
}
// 开启伪静态路径

View File

@@ -0,0 +1,8 @@
#theme-simple #announcement-content {
background-color: #f6f6f6;
}
.notion {
margin-top: 0 !important;
margin-bottom: 0 !important;
}

View File

@@ -215,4 +215,20 @@ nav {
/* twikoo 评论区超链接样式 */
.tk-main a {
@apply text-blue-700
}
}
/* 菜单下划线动画 */
.menu-link {
text-decoration: none;
background-image: linear-gradient(#dd3333, #dd3333);
background-repeat: no-repeat;
background-position: bottom center;
background-size: 0 2px;
transition: background-size 100ms ease-in-out;
}
.menu-link:hover {
background-size: 100% 2px;
color: #dd3333;
}

View File

@@ -1118,10 +1118,11 @@ code[class*='language-'] {
.notion-table-of-contents {
width: 100%;
margin: 4px 0;
@apply bg-gray-50 dark:bg-black p-2
}
.notion-table-of-contents-item {
color: inherit;
/* color: inherit; */
text-decoration: none;
user-select: none;
transition: background 20ms ease-in 0s;
@@ -1137,6 +1138,8 @@ code[class*='language-'] {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@apply text-blue-600 dark:text-blue-200
}
.notion-table-of-contents-item:hover {

View File

@@ -16,7 +16,7 @@ import BLOG from '@/blog.config'
const LayoutBase = props => {
const { children, meta } = props
return (
<div className='dark:text-gray-300 bg-white dark:bg-black'>
<div id='theme-example' className='dark:text-gray-300 bg-white dark:bg-black'>
<CommonHead meta={meta} />
{/* 顶栏LOGO */}
<Header {...props} />

View File

@@ -21,5 +21,5 @@ export const Header = (props) => {
</div>
</div>
</header>
);
)
}

View File

@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG_EMPTY from '../config_empty'
import CONFIG_EXAMPLE from '../config_example'
/**
* 菜单导航
@@ -11,10 +11,10 @@ export const Nav = (props) => {
const { customNav } = props
const { locale } = useGlobal()
let links = [
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_EMPTY.MENU_SEARCH },
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_EMPTY.MENU_ARCHIVE },
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_EMPTY.MENU_CATEGORY },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_EMPTY.MENU_TAG }
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_EXAMPLE.MENU_SEARCH },
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_EXAMPLE.MENU_ARCHIVE },
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_EXAMPLE.MENU_CATEGORY },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_EXAMPLE.MENU_TAG }
]
if (customNav) {

View File

@@ -1,8 +1,8 @@
const CONFIG_EMPTY = {
const CONFIG_EXAMPLE = {
// 菜单配置
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索
}
export default CONFIG_EMPTY
export default CONFIG_EXAMPLE

View File

@@ -1,4 +1,4 @@
import CONFIG_EMPTY from './config_empty'
import CONFIG_EXAMPLE from './config_example'
import { LayoutIndex } from './LayoutIndex'
import { LayoutSearch } from './LayoutSearch'
import { LayoutArchive } from './LayoutArchive'
@@ -11,7 +11,7 @@ import { LayoutTag } from './LayoutTag'
import { LayoutTagIndex } from './LayoutTagIndex'
export {
CONFIG_EMPTY as THEME_CONFIG,
CONFIG_EXAMPLE as THEME_CONFIG,
LayoutIndex,
LayoutSearch,
LayoutArchive,

View File

@@ -25,7 +25,7 @@ const LayoutBase = (props) => {
meta
} = props
const leftAreaSlot = <Live2D/>
return (<>
return (<div id='theme-fukasawa' >
<CommonHead meta={meta} />
<TopNav {...props}/>
<div className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + ' flex'}>
@@ -38,7 +38,7 @@ const LayoutBase = (props) => {
</main>
</div>
</>)
</div>)
}
export default LayoutBase

View File

@@ -59,7 +59,7 @@ const LayoutBase = props => {
}, [show])
return (
<div className="bg-hexo-background-gray dark:bg-black">
<div id='theme-hexo' className="bg-hexo-background-gray dark:bg-black">
<CommonHead meta={meta} siteInfo={siteInfo}/>
<TopNav {...props} />

View File

@@ -8,6 +8,25 @@ import * as medium from './medium'
import * as nobelium from './nobelium'
import * as matery from './matery'
import * as example from './example'
import * as simple from './simple'
export const ALL_THEME = ['hexo', 'matery', 'next', 'medium', 'fukasawa', 'nobelium', 'example']
export { hexo, next, medium, fukasawa, nobelium, matery, example }
export const ALL_THEME = [
'hexo',
'matery',
'next',
'medium',
'fukasawa',
'nobelium',
'example',
'simple'
]
export {
hexo,
next,
medium,
fukasawa,
nobelium,
matery,
example,
simple
}

View File

@@ -43,7 +43,7 @@ const LayoutBase = props => {
}, [show])
return (
<div id="outer-wrapper" className="min-h-screen flex flex-col justify-between bg-hexo-background-gray dark:bg-black w-full">
<div id='theme-matery' className="min-h-screen flex flex-col justify-between bg-hexo-background-gray dark:bg-black w-full">
<CommonHead meta={meta} siteInfo={siteInfo} />

View File

@@ -28,7 +28,7 @@ const LayoutBase = props => {
return (
<ThemeGlobalMedium.Provider value={{ tocVisible, changeTocVisible }}>
<div className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'>
<div id='theme-medium' className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'>
<CommonHead meta={meta} />
<main id='wrapper' className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + 'relative flex justify-between w-full h-full mx-auto'}>

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import CONFIG_MEDIUM from '../config_medium'
function GroupMenu ({ customNav }) {
function GroupMenu ({ customMenu, customNav }) {
const { locale } = useGlobal()
const router = useRouter()
@@ -39,13 +39,13 @@ function GroupMenu ({ customNav }) {
{link.slot}
</Link>
);
)
} else {
return null
}
})}
</nav>
);
)
}
export default GroupMenu

View File

@@ -74,7 +74,7 @@ export default function TopNavBar(props) {
{link.slot}
</Link>
);
)
} else {
return null
}
@@ -82,5 +82,5 @@ export default function TopNavBar(props) {
</div>
</div>
</div>
);
)
}

View File

@@ -59,7 +59,7 @@ const LayoutBase = (props) => {
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<>
return (<div id='theme-next'>
<CommonHead meta={meta} />
@@ -90,7 +90,7 @@ const LayoutBase = (props) => {
</div>
<Footer title={siteInfo?.title}/>
</>
</div>
)
}

View File

@@ -16,7 +16,7 @@ const LayoutBase = props => {
const fullWidth = post?.fullWidth ?? false
return (
<div className='nobelium relative dark:text-gray-300 w-full bg-white dark:bg-black min-h-screen'>
<div id='theme-nobelium' className='nobelium relative dark:text-gray-300 w-full bg-white dark:bg-black min-h-screen'>
<CommonHead meta={meta} />
{/* 顶部导航栏 */}

View File

@@ -0,0 +1,7 @@
import LayoutBase from './LayoutBase'
export const Layout404 = (props) => {
return <LayoutBase {...props}>
404 Not found.
</LayoutBase>
}

View File

@@ -0,0 +1,45 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutArchive = props => {
const { archivePosts } = props
return (
<LayoutBase {...props}>
<div className="mb-10 pb-20 md:py-12 p-3 min-h-screen w-full">
{Object.keys(archivePosts).map(archiveTitle => (
<div key={archiveTitle}>
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
{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>{' '}
&nbsp;
<Link
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
{post.title}
</Link>
</div>
</li>
))}
</ul>
</div>
))}
</div>
</LayoutBase>
)
}

View File

@@ -0,0 +1,60 @@
import CommonHead from '@/components/CommonHead'
import React from 'react'
import { Header } from './components/Header'
import { Nav } from './components/Nav'
import { Footer } from './components/Footer'
// import { Title } from './components/Title'
import { SideBar } from './components/SideBar'
import JumpToTopButton from './components/JumpToTopButton'
import BLOG from '@/blog.config'
import { TopBar } from './components/TopBar'
import CONFIG_SIMPLE from './config_simple'
import { isBrowser, loadExternalResource } from '@/lib/utils'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = props => {
const { children, meta } = props
if (isBrowser()) {
loadExternalResource('/css/theme-simple.css', 'css')
}
return (
<div id='theme-simple' className='dark:text-gray-300 bg-white dark:bg-black'>
<CommonHead meta={meta} />
{CONFIG_SIMPLE.TOP_BAR && <TopBar {...props}/>}
{/* 顶部LOGO */}
<Header {...props} />
{/* 菜单 */}
<Nav {...props} />
{/* 主体 */}
<div id='container-inner' className="w-full relative z-10">
{/* <Title {...props} /> */}
<div className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + 'w-full relative container mx-auto justify-start md:flex items-start py-8 px-2'}>
<div className='w-full max-w-6xl'>{children}</div>
<SideBar {...props} />
</div>
</div>
<Footer {...props} />
<div className='fixed right-4 bottom-4 z-10'>
<JumpToTopButton />
</div>
</div>
)
}
export default LayoutBase

View File

@@ -0,0 +1,10 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import LayoutBase from './LayoutBase'
export const LayoutCategory = props => {
return <LayoutBase {...props}>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase >
}

View File

@@ -0,0 +1,26 @@
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutCategoryIndex = props => {
const { categoryOptions } = props
return (
<LayoutBase {...props}>
<div id='category-list' className='duration-200 flex flex-wrap'>
{categoryOptions?.map(category => {
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<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>
</LayoutBase>
)
}

View File

@@ -0,0 +1,13 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import LayoutBase from './LayoutBase'
export const LayoutIndex = props => {
return (
<LayoutBase {...props}>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase>
)
}

View File

@@ -0,0 +1,10 @@
import { BlogListPage } from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutPage = props => {
return (
<LayoutBase {...props}>
<BlogListPage {...props} />
</LayoutBase>
)
}

View File

@@ -0,0 +1,54 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import SearchInput from './components/SearchInput'
import Mark from 'mark.js'
import LayoutBase from './LayoutBase'
import { isBrowser } from '@/lib/utils'
export const LayoutSearch = props => {
const { keyword } = props
const router = useRouter()
useEffect(() => {
setTimeout(() => {
const container = isBrowser() && document.getElementById('container')
if (container && container.innerHTML) {
const re = new RegExp(keyword, 'gim')
const instance = new Mark(container)
instance.markRegExp(re, {
element: 'span',
className: 'text-red-500 border-b border-dashed'
})
}
}, 100)
}, [router.events])
useEffect(() => {
setTimeout(() => {
if (keyword) {
const targets = document.getElementsByClassName('replace')
for (const container of targets) {
if (container && container.innerHTML) {
const re = new RegExp(`${keyword}`, 'gim')
container.innerHTML = container.innerHTML.replace(
re,
`<span class='text-red-500 border-b border-dashed'>${keyword}</span>`
)
}
}
}
}, 100)
}, [])
return <LayoutBase {...props}>
<div className='pb-12'>
<SearchInput {...props} />
</div>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase>
}

View File

@@ -0,0 +1,30 @@
import LayoutBase from './LayoutBase'
import { ArticleLock } from './components/ArticleLock'
import NotionPage from '@/components/NotionPage'
import { ArticleInfo } from './components/ArticleInfo'
import Comment from '@/components/Comment'
export const LayoutSlug = props => {
const { post, lock, validPassword } = props
if (!post) {
return <LayoutBase {...props} />
}
return (
<LayoutBase {...props}>
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && <div id="notion-article" className="px-2">
{post && <>
<ArticleInfo post={post} />
<NotionPage post={post} />
<Comment frontMatter={post}/>
</>}
</div>}
</LayoutBase>
)
}

View File

@@ -0,0 +1,10 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import LayoutBase from './LayoutBase'
export const LayoutTag = props => {
return <LayoutBase {...props}>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase >
}

View File

@@ -0,0 +1,29 @@
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = (props) => {
const { tagOptions } = props
return (
<LayoutBase {...props}>
<div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{tagOptions.map(tag => {
return (
<div key={tag.name} className='p-2'>
<Link
key={tag}
href={`/tag/${encodeURIComponent(tag.name)}`}
passHref
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>
</Link>
</div>
)
})}
</div>
</div> </LayoutBase>
)
}

View File

@@ -0,0 +1,21 @@
import BLOG from '@/blog.config'
import Router from 'next/router'
import React from 'react'
import SocialButton from './SocialButton'
const About = (props) => {
const { siteInfo } = props
return <>
<div className='flex flex-col items-center justify-center '>
<div className='hover:rotate-45 hover:scale-125 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/') }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={siteInfo?.icon} className='rounded-full' width={120} alt={BLOG.AUTHOR}/>
</div>
<div className='text-2xl font-serif dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.AUTHOR}</div>
<div className='font-light dark:text-white py-2 hover:scale-105 transform duration-200 text-center'>{BLOG.BIO}</div>
<SocialButton/>
</div>
</>
}
export default About

View File

@@ -0,0 +1,13 @@
import dynamic from 'next/dynamic'
const NotionPage = dynamic(() => import('@/components/NotionPage'))
const Announcement = ({ post, className }) => {
if (!post) {
return <></>
}
return <>{post && (<div id="announcement-content">
<NotionPage post={post} className='text-center ' />
</div>)} </>
}
export default Announcement

View File

@@ -0,0 +1,52 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import formatDate from '@/lib/formatDate'
export const ArticleInfo = (props) => {
const { post } = props
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
return (
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
<div>
{post?.type !== 'Page' && <>
<Link
href={`/category/${post.category}`}
passHref
className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
<i className="mr-1 fas fa-folder-open" />
{post.category}
</Link>
<span className='mr-2'>|</span>
</>}
{post?.type !== 'Page' && (<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
{date}
</Link>
<span className='mr-2'>|</span>
<span className='mx-2 text-gray-400 dark:text-gray-500'>
{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedTime}
</span>
<span className='mr-2'>|</span>
<span className="hidden busuanzi_container_page_pv font-light mr-2">
<i className='mr-1 fas fa-eye' />
&nbsp;
<span className="mr-2 busuanzi_value_page_pv" />
</span>
</>)}
</div>
</section>
)
}

View File

@@ -0,0 +1,38 @@
import { useGlobal } from '@/lib/global'
/**
* 加密文章校验组件
* @param {password, validPassword} props
* @param password 正确的密码
* @param validPassword(bool) 回调函数校验正确回调入参为true
* @returns
*/
export const ArticleLock = props => {
const { validPassword } = props
const { locale } = useGlobal()
const submitPassword = () => {
const p = document.getElementById('password')
if (!validPassword(p?.value)) {
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 id='container' className='w-full flex justify-center items-center h-96 font-sans'>
<div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'>
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'></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 dark:text-black'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,69 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import Link from 'next/link'
export const BlogListPage = props => {
const { page = 1, posts, postCount } = props
const { locale } = useGlobal()
const router = useRouter()
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const currentPage = +page
const showPrev = currentPage > 1
const showNext = page < totalPage
const pagePrefix = router.asPath.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
return (
<div className="w-full md:pr-12 mb-12">
<div id="container">
{posts?.map(p => (
<article key={p.id} className="mb-12" >
<h2 className="mb-4">
<Link
href={`/${p.slug}`}
className="text-black dark:text-gray-100 text-xl md:text-2xl no-underline hover:underline">
{p.title}
</Link>
</h2>
<div className="mb-4 text-sm text-gray-700 dark:text-gray-300">
<i className='fas fa-user'/> <a href="#" className="text-gray-700 dark:text-gray-300">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
<span className="font-bold mx-1"> | </span>
<a href={`/category${p.category}`} className="text-gray-700 dark:text-gray-300 hover:underline">{p.category}</a>
{/* <span className="font-bold mx-1"> | </span> */}
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
</div>
<p className="text-gray-700 dark:text-gray-400 leading-normal">
{p.summary}
</p>
{/* 搜索结果 */}
{p.results && (
<p className="mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
{p.results.map(r => (
<span key={r}>{r}</span>
))}
</p>
)}
</article>
))}
</div>
<div className="flex justify-between text-xs">
<Link
href={{ pathname: currentPage - 1 === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`, query: router.query.s ? { s: router.query.s } : {} }}
className={`${showPrev ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}>
{locale.PAGINATION.PREV}
</Link>
<Link
href={{ pathname: `${pagePrefix}/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}
className={`${showNext ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}>
{locale.PAGINATION.NEXT}
</Link>
</div>
</div>
)
}

View File

@@ -0,0 +1,82 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import React from 'react'
import throttle from 'lodash.throttle'
export const BlogListScroll = props => {
const { posts } = props
const { locale } = useGlobal()
const [page, updatePage] = React.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)
}
const targetRef = React.useRef(null)
// 监听滚动自动分页加载
const scrollTrigger = React.useCallback(throttle(() => {
const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
if (scrollS > clientHeight + 100) {
handleGetMore()
}
}, 500))
React.useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
})
return (
<div id="container" className="w-full md:pr-12 mb-12" ref={targetRef}>
{postsToShow.map(p => (
<article key={p.id} className="mb-12" >
<h2 className="mb-4">
<Link
href={`/${p.slug}`}
className="text-black text-xl md:text-2xl no-underline hover:underline">
{p.title}
</Link>
</h2>
<div className="mb-4 text-sm text-gray-700">
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
<span className="font-bold mx-1"> | </span>
<a href="#" className="text-gray-700">{p.category}</a>
<span className="font-bold mx-1"> | </span>
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
</div>
<p className="text-gray-700 leading-normal">
{p.summary}
</p>
</article>
))}
<div
onClick={handleGetMore}
className="w-full my-4 py-4 text-center cursor-pointer "
>
{' '}
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
</div>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import Link from 'next/link'
import { useState } from 'react'
export const DropMenu = ({ link }) => {
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
return <div className='my-4 mr-4'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
>
<Link
href={link?.to}
className="menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest">
{link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
{hasSubMenu && <ul className={`${show ? 'visible opacity-100' : 'invisible opacity-0'} transition-all duration-300 z-20 top-12 absolute block border border-gray-100 bg-white drop-shadow-lg `}>
{link.subMenus.map(sLink => {
return <li key={sLink.id} className=' text-blue-400 hover:bg-gray-50 tracking-widest transition-all duration-200 border-b py-3 pr-6 pl-2'>
<Link href={sLink.to}>
<span className='text-xs'>{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</div>
}

View File

@@ -0,0 +1,35 @@
import React from 'react'
import BLOG from '@/blog.config'
import Link from 'next/link'
import { RecentComments } from '@waline/client'
/**
* @see https://waline.js.org/guide/get-started.html
* @param {*} props
* @returns
*/
const ExampleRecentComments = (props) => {
const [comments, updateComments] = React.useState([])
const [onLoading, changeLoading] = React.useState(true)
React.useEffect(() => {
RecentComments({
serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
count: 5
}).then(({ comments }) => {
changeLoading(false)
updateComments(comments)
})
}, [])
return <>
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
<div className='dark:text-gray-400 text-gray-400 font-sans text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</Link></div>
</div>)}
</>
}
export default ExampleRecentComments

View File

@@ -0,0 +1,30 @@
import BLOG from '@/blog.config'
import DarkModeButton from '@/components/DarkModeButton'
export const Footer = (props) => {
const d = new Date()
const currentYear = d.getFullYear()
const copyrightDate = (function() {
if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
return BLOG.SINCE + '-' + currentYear
}
return currentYear
})()
return <footer className="z-10 relative w-full bg-white px-6 border-t dark:border-hexo-black-gray dark:bg-hexo-black-gray ">
<DarkModeButton className='text-center pt-4'/>
<div className="container mx-auto max-w-4xl py-6 md:flex flex-wrap md:flex-no-wrap md:justify-between items-center text-sm">
<div className='text-center'> &copy;{`${copyrightDate}`} {BLOG.AUTHOR}. All rights reserved.</div>
<div className="md:p-0 text-center md:text-right text-xs">
{/* 右侧链接 */}
{/* <a href="#" className="text-black no-underline hover:underline">Privacy Policy</a> */}
{BLOG.BEI_AN && (<a href="https://beian.miit.gov.cn/" className="text-black dark:text-gray-200 no-underline hover:underline ml-4">{BLOG.BEI_AN} </a>)}
<span className='dark:text-gray-200 no-underline ml-4'>
Powered by
<a href="https://github.com/tangly1024/NotionNext" className=' hover:underline'> NotionNext {BLOG.VERSION} </a>
</span>
</div>
</div>
</footer>
}

View File

@@ -0,0 +1,22 @@
import Link from 'next/link'
import CONFIG_SIMPLE from '../config_simple'
/**
* 网站顶部
* @returns
*/
export const Header = (props) => {
const { siteInfo } = props
return (
<header className="text-center justify-between items-center px-6 bg-white h-80 dark:bg-black relative z-10">
<div className="float-none inline-block py-12">
<Link href='/'>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className='max-h-48 hover:opacity-60 duration-200 transition-all cursor-pointer' src={CONFIG_SIMPLE.LOGO_IMG}/>
</Link>
<div className='text-xs text-gray-600 dark:text-gray-300'>{siteInfo?.description}</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,19 @@
import { useGlobal } from '@/lib/global'
import React from 'react'
/**
* 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件
* @param targetRef 关联高度的目标html标签
* @param showPercent 是否显示百分比
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = () => {
const { locale } = useGlobal()
return <div title={locale.POST.TOP} className='cursor-pointer p-2 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
><i className='fas fa-angle-up text-2xl' />
</div>
}
export default JumpToTopButton

View File

@@ -0,0 +1,51 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import CONFIG_SIMPLE from '../config_simple'
import { DropMenu } from './DropMenu'
/**
* 菜单导航
* @param {*} props
* @returns
*/
export const Nav = ({ customNav, customMenu }) => {
const { locale } = useGlobal()
let links = [
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_SIMPLE.MENU_SEARCH },
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_SIMPLE.MENU_ARCHIVE },
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_SIMPLE.MENU_CATEGORY },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_SIMPLE.MENU_TAG }
]
if (customNav) {
links = links.concat(customNav)
}
if (BLOG.CUSTOM_MENU) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return (
<nav className="w-full bg-white md:pt-0 px-6 relative z-20 shadow-md border-t border-gray-100 dark:border-hexo-black-gray dark:bg-black">
<div className="container mx-auto max-w-8xl md:flex justify-between items-center text-sm md:text-md md:justify-start">
<div className="w-full text-center md:text-left flex flex-wrap justify-center items-stretch md:justify-start md:items-start">
{links?.map(link => {
if (link?.show) {
return <DropMenu key={link.id} link={link}/>
} else {
return null
}
})}
</div>
<div className="w-full md:w-1/3 text-center md:text-right">
{/* <!-- extra links --> */}
<i className='fa'></i>
</div>
</div>
</nav>
)
}

View File

@@ -0,0 +1,87 @@
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

View File

@@ -0,0 +1,24 @@
import Live2D from '@/components/Live2D'
import About from './About'
import Announcement from './Announcement'
export const SideBar = (props) => {
const { notice } = props
return (
<div className="w-full md:w-96 sticky top-8 border-l pl-12 border-gray-100">
<aside>
<Announcement post={notice}/>
</aside>
<aside>
<About {...props}/>
</aside>
<aside className=" overflow-hidden mb-6">
<Live2D />
</aside>
</div>
)
}

View File

@@ -0,0 +1,36 @@
import BLOG from '@/blog.config'
import React from 'react'
/**
* 社交联系方式按钮组
* @returns {JSX.Element}
* @constructor
*/
const SocialButton = () => {
return <div className='w-52 justify-center flex-wrap flex'>
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 text-center'>
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
<i className='fab fa-github transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_TWITTER && <a target='_blank' rel='noreferrer' title={'twitter'} href={BLOG.CONTACT_TWITTER} >
<i className='fab fa-twitter transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_TELEGRAM && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_TELEGRAM} title={'telegram'} >
<i className='fab fa-telegram transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_LINKEDIN && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_LINKEDIN} title={'linkedIn'} >
<i className='transform hover:scale-125 duration-150 fab fa-linkedin dark:hover:text-indigo-400 hover:text-indigo-600'/>
</a>}
{BLOG.CONTACT_WEIBO && <a target='_blank' rel='noreferrer' title={'weibo'} href={BLOG.CONTACT_WEIBO} >
<i className='fab fa-weibo transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
<i className='fas fa-envelope transform hover:scale-125 duration-150'/>
</a>}
<a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
<i className='fas fa-rss transform hover:scale-125 duration-150'/>
</a>
</div>
</div>
}
export default SocialButton

View File

@@ -0,0 +1,19 @@
import BLOG from '@/blog.config'
/**
* 标题栏
* @param {*} props
* @returns
*/
export const Title = (props) => {
const { siteInfo, post } = props
const title = post?.title || siteInfo?.description
const description = post?.description || BLOG.AUTHOR
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
<p className="leading-loose text-gray-dark">
{description}
</p>
</div>
}

View File

@@ -0,0 +1,13 @@
import CONFIG_SIMPLE from '../config_simple'
/**
* 网站顶部
* @returns
*/
export const TopBar = (props) => {
return (
<header className="w-full flex justify-between items-center px-20 h-10 bg-black dark:bg-hexo-black-gray z-10">
<div className='text-xs text-white' dangerouslySetInnerHTML={{ __html: CONFIG_SIMPLE.TOP_BAR_CONTENT }}/>
</header>
)
}

View File

@@ -0,0 +1,13 @@
const CONFIG_SIMPLE = {
LOGO_IMG: '/Logo.webp',
TOP_BAR: true, // 显示顶栏
TOP_BAR_CONTENT: '<div>欢迎访问我的网站NotionNext最新版本已更新至3.12.0,欢迎体验!<div>',
// 菜单配置
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索
}
export default CONFIG_SIMPLE

25
themes/simple/index.js Normal file
View File

@@ -0,0 +1,25 @@
import CONFIG_SIMPLE from './config_simple'
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_SIMPLE as THEME_CONFIG,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404,
LayoutCategory,
LayoutCategoryIndex,
LayoutPage,
LayoutTag,
LayoutTagIndex
}