mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
theme-plog
This commit is contained in:
48
components/FullScreenButton.js
Normal file
48
components/FullScreenButton.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 全屏按钮
|
||||
* @returns
|
||||
*/
|
||||
const FullScreenButton = () => {
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
|
||||
const handleFullScreenClick = () => {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
const element = document.documentElement
|
||||
if (!isFullScreen) {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen()
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen()
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen()
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen()
|
||||
}
|
||||
setIsFullScreen(true)
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen()
|
||||
}
|
||||
setIsFullScreen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleFullScreenClick} className='dark:text-gray-300'>
|
||||
{isFullScreen ? '退出全屏' : <i className="fa-solid fa-expand"></i>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default FullScreenButton
|
||||
@@ -72,7 +72,7 @@ const Hero = props => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='header-cover' style={{ backgroundImage: `url('${siteInfo.pageCover}')` }}
|
||||
<div id='header-cover' style={{ backgroundImage: `url('${siteInfo?.pageCover}')` }}
|
||||
className={`header-cover bg-center w-full h-screen bg-cover ${CONFIG.HOME_NAV_BACKGROUND_IMG_FIXED ? 'bg-fixed' : ''}`} />
|
||||
|
||||
</header>
|
||||
|
||||
@@ -64,7 +64,7 @@ const Hero = props => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='header-cover' style={{ backgroundImage: `url('${siteInfo.pageCover}')` }}
|
||||
<div id='header-cover' style={{ backgroundImage: `url('${siteInfo?.pageCover}')` }}
|
||||
className={`header-cover bg-center w-full h-screen bg-cover ${CONFIG.HOME_NAV_BACKGROUND_IMG_FIXED ? 'bg-fixed' : ''}`} />
|
||||
|
||||
</header>
|
||||
|
||||
18
themes/plog/components/Announcement.js
Normal file
18
themes/plog/components/Announcement.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const NotionPage = dynamic(() => import('@/components/NotionPage'))
|
||||
|
||||
const Announcement = ({ notice, className }) => {
|
||||
if (notice?.blockMap) {
|
||||
return <div className={className}>
|
||||
<section id='announcement-wrapper' className='mb-10'>
|
||||
{notice && (<div id="announcement-content">
|
||||
<NotionPage post={notice} className='text-center ' />
|
||||
</div>)}
|
||||
</section>
|
||||
</div>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
export default Announcement
|
||||
34
themes/plog/components/ArticleFooter.js
Normal file
34
themes/plog/components/ArticleFooter.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleFooter = props => {
|
||||
const router = useRouter()
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <div className="flex justify-between font-medium text-gray-500 dark:text-gray-400">
|
||||
<a>
|
||||
<button
|
||||
onClick={() => router.push(BLOG.path || '/')}
|
||||
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
|
||||
>
|
||||
← {locale.POST.BACK}
|
||||
</button>
|
||||
</a>
|
||||
<a>
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
|
||||
>
|
||||
↑ {locale.POST.TOP}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
59
themes/plog/components/ArticleInfo.js
Normal file
59
themes/plog/components/ArticleInfo.js
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import Image from 'next/image'
|
||||
import BLOG from '@/blog.config'
|
||||
import TagItem from './TagItem'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
export const ArticleInfo = (props) => {
|
||||
const { post } = props
|
||||
|
||||
const emailHash = md5(BLOG.CONTACT_EMAIL)
|
||||
|
||||
return <section className="flex-wrap flex mt-2 text-gray--600 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
|
||||
<div className="font-bold text-3xl text-black dark:text-white">
|
||||
{post?.title}
|
||||
</div>
|
||||
|
||||
{post?.type !== 'Page' && <>
|
||||
<nav className="flex mt-7 items-start text-gray-500 dark:text-gray-400">
|
||||
<div className="flex mb-4">
|
||||
<a href={BLOG.CONTACT_GITHUB || '#'} className="flex">
|
||||
<Image
|
||||
alt={BLOG.author}
|
||||
width={24}
|
||||
height={24}
|
||||
src={`https://gravatar.com/avatar/${emailHash}`}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<p className="ml-2 md:block">{BLOG.author}</p>
|
||||
</a>
|
||||
<span className="block"> / </span>
|
||||
</div>
|
||||
<div className="mr-2 mb-4 md:ml-0">
|
||||
{formatDate(
|
||||
post?.publishTime || post?.createdTime,
|
||||
BLOG.LANG
|
||||
)}
|
||||
</div>
|
||||
{post.tags && (
|
||||
<div className="flex flex-nowrap max-w-full overflow-x-auto article-tags">
|
||||
{post?.tags.map(tag => (
|
||||
<TagItem key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden busuanzi_container_page_pv mr-2">
|
||||
<i className='mr-1 fas fa-eye' />
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv" />
|
||||
</span>
|
||||
</nav>
|
||||
</>}
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
}
|
||||
53
themes/plog/components/ArticleLock.js
Normal file
53
themes/plog/components/ArticleLock.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @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>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const passwordInputRef = useRef(null)
|
||||
useEffect(() => {
|
||||
// 选中密码输入框并将其聚焦
|
||||
passwordInputRef.current.focus()
|
||||
}, [])
|
||||
|
||||
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'>
|
||||
<input id="password" type='password'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitPassword()
|
||||
}
|
||||
}}
|
||||
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
|
||||
className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg 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'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
41
themes/plog/components/BlogArchiveItem.js
Normal file
41
themes/plog/components/BlogArchiveItem.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 归档分组文章
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
|
||||
return (
|
||||
<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?.publishTime}>
|
||||
<span className="text-gray-400">
|
||||
{post.date?.start_date}
|
||||
</span>{' '}
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
39
themes/plog/components/BlogListBar.js
Normal file
39
themes/plog/components/BlogListBar.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Tags from './Tags'
|
||||
|
||||
export default function BlogListBar(props) {
|
||||
const { tag, setFilterKey } = props
|
||||
const handleSearchChange = (val) => {
|
||||
setFilterKey(val)
|
||||
}
|
||||
if (tag) {
|
||||
return (<div className="mb-4">
|
||||
<div className='relative'>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
tag ? `Search in #${tag}` : 'Search Articles'
|
||||
}
|
||||
className="outline-none block w-full border px-4 py-2 border-black bg-white text-black dark:bg-night dark:border-white dark:text-white"
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-3 h-5 w-5 text-black dark:text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<Tags {...props} />
|
||||
</div>)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
50
themes/plog/components/BlogListPage.js
Normal file
50
themes/plog/components/BlogListPage.js
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import BlogPost from './BlogPost'
|
||||
|
||||
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.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
<div id="posts-wrapper" className='grid lg:grid-cols-3 grid-cols-2'>
|
||||
{posts?.map(post => (
|
||||
<BlogPost key={post.id} post={post} {...props}/>
|
||||
))}
|
||||
</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 ? ' ' : ' invisible block pointer-events-none '}no-underline py-2 px-3 rounded`}>
|
||||
|
||||
<button rel="prev" className="block cursor-pointer">
|
||||
← {locale.PAGINATION.PREV}
|
||||
</button>
|
||||
|
||||
</Link>
|
||||
<Link
|
||||
href={{ pathname: `${pagePrefix}/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}
|
||||
className={`${showNext ? ' ' : 'invisible pointer-events-none '} no-underline py-2 px-3 rounded`}>
|
||||
|
||||
<button rel="next" className="block cursor-pointer">
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</button>
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
themes/plog/components/BlogListScroll.js
Normal file
82
themes/plog/components/BlogListScroll.js
Normal 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="posts-wrapper" 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>
|
||||
)
|
||||
}
|
||||
23
themes/plog/components/BlogPost.js
Normal file
23
themes/plog/components/BlogPost.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
/**
|
||||
* 博客照片卡牌
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const BlogPost = (props) => {
|
||||
const { post, siteInfo } = props
|
||||
const pageThumbnail = post?.pageCoverThumbnail || siteInfo?.pageCover
|
||||
console.log('缩略图', pageThumbnail, siteInfo)
|
||||
return (
|
||||
<article key={post?.id} className='cursor-pointer relative'>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={pageThumbnail} className='aspect-[16/9] w-full h-full object-cover' />
|
||||
<h2 className="text-md absolute left-0 bottom-0 mb-4 ml-4 text-black dark:text-gray-100 text-shadow">
|
||||
{post?.title}
|
||||
</h2>
|
||||
</article>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
88
themes/plog/components/BottomNav.js
Normal file
88
themes/plog/components/BottomNav.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG from '../config'
|
||||
import { SvgIcon } from './SvgIcon'
|
||||
import { MenuItemDrop } from './MenuItemDrop'
|
||||
import FullScreenButton from '@/components/FullScreenButton'
|
||||
|
||||
/**
|
||||
* 桌面端底部导航
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const BottomNav = props => {
|
||||
const { navBarTitle, siteInfo } = props
|
||||
|
||||
return <>
|
||||
<div id="bottom-nav" className={'px-4 hidden glassmorphism md:fixed bottom-0 w-screen py-4 md:flex flex-row justify-between items-center'}>
|
||||
<div className="flex items-center">
|
||||
<Link href="/" aria-label={BLOG.title}>
|
||||
<div className="h-6 w-6">
|
||||
{/* <SvgIcon/> */}
|
||||
{CONFIG.NAV_NOTION_ICON
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
? <img src={siteInfo?.icon} className='rounded-full' width={24} height={24} alt={BLOG.AUTHOR} />
|
||||
: <SvgIcon />}
|
||||
</div>
|
||||
</Link>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className="ml-2 font-medium text-gray-800 dark:text-gray-300 header-name">
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className="ml-2 font-medium text-gray-800 dark:text-gray-300 header-name">
|
||||
{siteInfo?.title}
|
||||
{' '}<span className="font-normal text-sm text-gray-00 dark:text-gray-400">{siteInfo?.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<MenuList {...props} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const MenuList = props => {
|
||||
const { customMenu, customNav } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
|
||||
]
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
// 如果 开启自定义菜单,则覆盖Page生成的菜单
|
||||
if (BLOG.CUSTOM_MENU) {
|
||||
links = customMenu
|
||||
}
|
||||
|
||||
if (!links || links.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<ul className="hidden md:flex flex-row">
|
||||
{links?.map(link => <MenuItemDrop key={link?.id} link={link} />)}
|
||||
<li className='my-auto px-2'>
|
||||
<FullScreenButton/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BottomNav
|
||||
35
themes/plog/components/ExampleRecentComments.js
Normal file
35
themes/plog/components/ExampleRecentComments.js
Normal 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
|
||||
34
themes/plog/components/Footer.js
Normal file
34
themes/plog/components/Footer.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import Vercel from '@/components/Vercel'
|
||||
|
||||
export const Footer = (props) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const { post } = props
|
||||
const fullWidth = post?.fullWidth ?? false
|
||||
|
||||
const copyrightDate = (function() {
|
||||
if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
|
||||
return BLOG.SINCE + '-' + currentYear
|
||||
}
|
||||
return currentYear
|
||||
})()
|
||||
|
||||
return <footer
|
||||
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 transition-all ${
|
||||
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
>
|
||||
<DarkModeButton className='text-center pt-4'/>
|
||||
<hr className="border-gray-200 dark:border-gray-600" />
|
||||
<div className="my-4 text-sm leading-6">
|
||||
<div className="flex align-baseline justify-between flex-wrap">
|
||||
<p>
|
||||
© {BLOG.AUTHOR} {copyrightDate}
|
||||
</p>
|
||||
<Vercel />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
19
themes/plog/components/JumpToTopButton.js
Normal file
19
themes/plog/components/JumpToTopButton.js
Normal 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
|
||||
55
themes/plog/components/MenuItemCollapse.js
Normal file
55
themes/plog/components/MenuItemCollapse.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import Collapse from '@/components/Collapse'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 折叠菜单
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const MenuItemCollapse = (props) => {
|
||||
const { link } = props
|
||||
const [show, changeShow] = useState(false)
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
const [isOpen, changeIsOpen] = useState(false)
|
||||
|
||||
const toggleShow = () => {
|
||||
changeShow(!show)
|
||||
}
|
||||
|
||||
const toggleOpenSubMenu = () => {
|
||||
changeIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} >
|
||||
{!hasSubMenu && <Link
|
||||
href={link?.to}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
</Link>}
|
||||
{hasSubMenu && <div
|
||||
onClick={hasSubMenu ? toggleOpenSubMenu : null}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
<i className='px-2 fa fa-plus text-gray-400'></i>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* 折叠子菜单 */}
|
||||
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
||||
{link.subMenus.map(sLink => {
|
||||
return <div key={sLink.id} className='font-extralight dark:bg-black text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 border-b dark:border-gray-800 py-3 pr-6'>
|
||||
<Link href={sLink.to}>
|
||||
<span className='text-xs'>{sLink.title}</span>
|
||||
</Link>
|
||||
</div>
|
||||
})}
|
||||
</Collapse>}
|
||||
</>
|
||||
}
|
||||
43
themes/plog/components/MenuItemDrop.js
Normal file
43
themes/plog/components/MenuItemDrop.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MenuItemDrop = ({ link }) => {
|
||||
const [show, changeShow] = useState(false)
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
return <li className='mx-3 my-2' >
|
||||
<div className='cursor-pointer ' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}>
|
||||
{!hasSubMenu &&
|
||||
<div className="block text-black dark:text-gray-50 nav" >
|
||||
<Link href={link?.to} >
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
{hasSubMenu &&
|
||||
<div className='block text-black dark:text-gray-50 nav'>
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* 子菜单 */}
|
||||
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 bottom-16 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white rounded-lg dark:bg-black dark:border-gray-800 bg-opacity-60 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
|
||||
{link.subMenus.map(sLink => {
|
||||
return <li key={sLink.id} className='text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
|
||||
<Link href={sLink.to}>
|
||||
<span className='text-sm text-nowrap font-extralight'>{link?.icon && <i className={sLink?.icon} > </i>}{sLink.title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
})}
|
||||
</ul>}
|
||||
|
||||
</div>
|
||||
|
||||
</li>
|
||||
}
|
||||
93
themes/plog/components/Nav.js
Normal file
93
themes/plog/components/Nav.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG from '../config'
|
||||
import { SvgIcon } from './SvgIcon'
|
||||
import { MenuItemDrop } from './MenuItemDrop'
|
||||
import Collapse from '@/components/Collapse'
|
||||
import { MenuItemCollapse } from './MenuItemCollapse'
|
||||
|
||||
const Nav = props => {
|
||||
const { navBarTitle, fullWidth, siteInfo } = props
|
||||
return <div className='md:hidden fixed top-0 w-full'>
|
||||
<div id="sticky-nav"
|
||||
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 md:mb-12 py-8 glassmorphism ${!fullWidth ? 'max-w-3xl px-4' : 'px-4 md:px-24'
|
||||
}`} >
|
||||
<div className="flex items-center">
|
||||
<Link href="/" aria-label={BLOG.title}>
|
||||
|
||||
<div className="h-6 w-6">
|
||||
{/* <SvgIcon/> */}
|
||||
{CONFIG.NAV_NOTION_ICON
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
? <img src={siteInfo?.icon} width={24} height={24} alt={BLOG.AUTHOR} />
|
||||
: <SvgIcon />}
|
||||
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className="ml-2 font-medium text-gray-800 dark:text-gray-300 header-name">
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className="ml-2 font-medium text-gray-800 dark:text-gray-300 header-name">
|
||||
{siteInfo?.title}
|
||||
{/* ,{' '}<span className="font-normal">{siteInfo?.description}</span> */}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar {...props} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const NavBar = props => {
|
||||
const { customMenu, customNav } = props
|
||||
const [isOpen, changeOpen] = useState(false)
|
||||
const toggleOpen = () => {
|
||||
changeOpen(!isOpen)
|
||||
}
|
||||
const collapseRef = useRef(null)
|
||||
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
|
||||
]
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
// 如果 开启自定义菜单,则覆盖Page生成的菜单
|
||||
if (BLOG.CUSTOM_MENU) {
|
||||
links = customMenu
|
||||
}
|
||||
|
||||
if (!links || links.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<ul className=" hidden md:flex flex-row">
|
||||
{links?.map(link => <MenuItemDrop key={link?.id} link={link} />)}
|
||||
</ul>
|
||||
<div className='md:hidden'><i onClick={toggleOpen} className='fas fa-bars cursor-pointer px-5 block md:hidden'></i>
|
||||
<Collapse collapseRef={collapseRef} isOpen={isOpen} type='vertical' className='fixed top-16 right-6'>
|
||||
<div className='dark:border-black bg-white dark:bg-black rounded border p-2 text-sm'>
|
||||
{links?.map(link => <MenuItemCollapse key={link?.id} link={link} onHeightChange={(param) => collapseRef.current?.updateCollapseHeight(param)}/>)}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
28
themes/plog/components/PlogModal.js
Normal file
28
themes/plog/components/PlogModal.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
/**
|
||||
* 图片弹出模态框
|
||||
*/
|
||||
export default function PlogModal(props) {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title>Deactivate account</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
This will permanently deactivate your account
|
||||
</Dialog.Description>
|
||||
|
||||
<p>
|
||||
Are you sure you want to deactivate your account? All of your data
|
||||
will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<button onClick={() => setIsOpen(false)}>Deactivate</button>
|
||||
<button onClick={() => setIsOpen(false)}>Cancel</button>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
87
themes/plog/components/SearchInput.js
Normal file
87
themes/plog/components/SearchInput.js
Normal 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={'outline-none 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
|
||||
17
themes/plog/components/SearchNavBar.js
Normal file
17
themes/plog/components/SearchNavBar.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import SearchInput from './SearchInput'
|
||||
import Tags from './Tags'
|
||||
|
||||
/**
|
||||
* 搜索页面上方嵌入内容
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function SearchNavBar(props) {
|
||||
return (<>
|
||||
<div className='pb-12'>
|
||||
<SearchInput {...props} />
|
||||
</div>
|
||||
|
||||
<Tags {...props}/>
|
||||
</>)
|
||||
}
|
||||
65
themes/plog/components/SideBar.js
Normal file
65
themes/plog/components/SideBar.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
|
||||
|
||||
export const SideBar = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { latestPosts, categories } = props
|
||||
return (
|
||||
<div className="w-full md:w-64 sticky top-8">
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.CATEGORY}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{categories?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.LATEST_POSTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{latestPosts?.map(p => {
|
||||
return (
|
||||
<Link key={p.id} href={`/${p.slug}`} passHref legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT && <aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.RECENT_COMMENTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ExampleRecentComments/>
|
||||
</div>
|
||||
</aside>}
|
||||
|
||||
<aside className="rounded overflow-hidden mb-6">
|
||||
<Live2D />
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
themes/plog/components/SvgIcon.js
Normal file
29
themes/plog/components/SvgIcon.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export const SvgIcon = () => {
|
||||
return <svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
className="fill-current text-black dark:text-white"
|
||||
/>
|
||||
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45) scale(39.598)"
|
||||
>
|
||||
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
||||
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
13
themes/plog/components/TagItem.js
Normal file
13
themes/plog/components/TagItem.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItem = ({ tag }) => (
|
||||
(<Link href={`/tag/${encodeURIComponent(tag)}`}>
|
||||
|
||||
<p className="mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600">
|
||||
{tag}
|
||||
</p>
|
||||
|
||||
</Link>)
|
||||
)
|
||||
|
||||
export default TagItem
|
||||
38
themes/plog/components/Tags.js
Normal file
38
themes/plog/components/Tags.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const Tags = props => {
|
||||
const { tagOptions, tag } = props
|
||||
const currentTag = tag
|
||||
if (!tagOptions) return null
|
||||
return (
|
||||
<div className="tag-container">
|
||||
<ul className="flex max-w-full mt-4 overflow-x-auto">
|
||||
{Object.keys(tagOptions).map(key => {
|
||||
const tag = tagOptions[key]
|
||||
const selected = tag.name === currentTag
|
||||
return (
|
||||
<li
|
||||
key={tag.id}
|
||||
className={`mr-3 font-medium border whitespace-nowrap dark:text-gray-300 ${
|
||||
selected
|
||||
? 'text-white bg-black border-black dark:bg-gray-600 dark:border-gray-600'
|
||||
: 'bg-gray-100 border-gray-100 text-gray-400 dark:bg-night dark:border-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={selected ? '/search' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||
className="px-4 py-2 block">
|
||||
|
||||
{`${tag.name} (${tag.count})`}
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tags
|
||||
19
themes/plog/components/Title.js
Normal file
19
themes/plog/components/Title.js
Normal 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>
|
||||
}
|
||||
13
themes/plog/config.js
Normal file
13
themes/plog/config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const CONFIG = {
|
||||
|
||||
// 菜单配置
|
||||
MENU_CATEGORY: false, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: false, // 显示归档
|
||||
MENU_SEARCH: true, // 显示搜索
|
||||
MENU_RSS: false, // 显示订阅
|
||||
|
||||
NAV_NOTION_ICON: true // 是否读取Notion图标作为站点头像
|
||||
|
||||
}
|
||||
export default CONFIG
|
||||
262
themes/plog/index.js
Normal file
262
themes/plog/index.js
Normal file
@@ -0,0 +1,262 @@
|
||||
import CONFIG from './config'
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import React, { useEffect } from 'react'
|
||||
import Nav from './components/Nav'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { BlogListPage } from './components/BlogListPage'
|
||||
import { BlogListScroll } from './components/BlogListScroll'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Mark from 'mark.js'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import SearchNavBar from './components/SearchNavBar'
|
||||
import BlogArchiveItem from './components/BlogArchiveItem'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import { ArticleInfo } from './components/ArticleInfo'
|
||||
import Comment from '@/components/Comment'
|
||||
import { ArticleFooter } from './components/ArticleFooter'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import Link from 'next/link'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import BottomNav from './components/BottomNav'
|
||||
import { saveDarkModeToCookies } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, meta, topSlot } = props
|
||||
const { onLoading, updateDarkMode } = useGlobal()
|
||||
|
||||
// 用户手动设置主题
|
||||
const setDarkMode = () => {
|
||||
saveDarkModeToCookies(true)
|
||||
updateDarkMode(true)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove('light')
|
||||
htmlElement.classList?.add('dark')
|
||||
}
|
||||
|
||||
// plog主题默认 深色模式
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDarkMode()
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id='theme-plog' className='plog relative dark:text-gray-300 w-full bg-black min-h-screen'>
|
||||
{/* SEO相关 */}
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
{/* 移动端顶部导航栏 */}
|
||||
<Nav {...props} />
|
||||
|
||||
{/* 主区 */}
|
||||
<main id='out-wrapper' className={'relative m-auto flex-grow w-full transition-all '}>
|
||||
|
||||
<Transition
|
||||
show={!onLoading}
|
||||
appear={true}
|
||||
enter="transition ease-in-out duration-700 transform order-first"
|
||||
enterFrom="opacity-0 translate-y-16"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-16"
|
||||
unmount={false}
|
||||
>
|
||||
{/* 顶部插槽 */}
|
||||
{topSlot}
|
||||
{children}
|
||||
</Transition>
|
||||
|
||||
</main>
|
||||
|
||||
{/* 桌面端底部导航栏 */}
|
||||
<BottomNav {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* 首页是个博客列表,加上顶部嵌入一个公告
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutIndex = props => {
|
||||
return (
|
||||
<LayoutPostList {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 博客列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutPostList = props => {
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* 页面是博客列表,上方嵌入一个搜索引导条
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSearch = props => {
|
||||
const { keyword } = props
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = isBrowser() && document.getElementById('posts-wrapper')
|
||||
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])
|
||||
|
||||
return <LayoutPostList {...props} slotTop={<SearchNavBar {...props} />} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
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 => <BlogArchiveItem key={archiveTitle} archiveTitle={archiveTitle} archivePosts={archivePosts} />)}
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{!lock && <div id="article-wrapper" className="px-2">
|
||||
<>
|
||||
<ArticleInfo post={post} />
|
||||
<NotionPage post={post} />
|
||||
<ShareBar post={post} />
|
||||
<Comment frontMatter={post} />
|
||||
<ArticleFooter />
|
||||
</>
|
||||
</div>}
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 页面
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Layout404 = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
404 Not found.
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章分类列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章标签列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
CONFIG as THEME_CONFIG,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
LayoutSlug,
|
||||
Layout404,
|
||||
LayoutPostList,
|
||||
LayoutCategoryIndex,
|
||||
LayoutTagIndex
|
||||
}
|
||||
Reference in New Issue
Block a user