完成部分页面

This commit is contained in:
tangly1024
2022-01-16 14:41:38 +08:00
parent d9438e6a79
commit 8216bee7db
29 changed files with 624 additions and 3 deletions

View File

@@ -7,6 +7,7 @@ const BLOG = {
link: 'https://tangly1024.com', // 网站地址
keywords: ['Notion', '写作', '博客'], // 网站关键词
lang: 'zh-CN', // ['zh-CN','en-US'] default lang => see /lib/lang.js for more.
beian: '闽ICP备20010331号', // 备案号
notionPageId: process.env.NOTION_PAGE_ID || 'bee1fccfa3bd47a1a7be83cc71372d83', // Important page_id
notionAccessToken: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
appearance: 'auto', // ['light', 'dark', 'auto'],

View File

@@ -36,7 +36,7 @@ const CommonHead = ({ meta }) => {
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
{meta.type === 'article' && (
{meta?.type === 'article' && (
<>
<meta
property='article:published_time'

View File

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

View File

@@ -0,0 +1,7 @@
import LayoutBase from './LayoutBase'
export const LayoutArchive = (props) => {
return <LayoutBase {...props}>
Archive Page
</LayoutBase>
}

View File

@@ -0,0 +1,48 @@
import CommonHead from '@/components/CommonHead'
import AsideLeft from './components/AsideLeft'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param children
* @param layout
* @param tags
* @param meta
* @param post
* @param currentSearch
* @param currentCategory
* @param currentTag
* @param categories
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = ({
children,
headerSlot,
tags,
meta,
post,
postCount,
sideBarSlot,
floatSlot,
rightAreaSlot,
currentSearch,
currentCategory,
currentTag,
categories
}) => {
return (<>
<CommonHead meta={meta} />
<div className='flex flex-between'>
<AsideLeft tags={tags} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
<main id='wrapper' className='flex flex-grow py-8 justify-center'>
<div className='2xl:max-w-6xl md:max-w-3xl max-w-md w-full'>
<div> {headerSlot} </div>
<div>{children}</div>
</div>
</main>
</div>
</>)
}
export default LayoutBase

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
export const LayoutSearch = (props) => {
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = props.posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = props.posts
}
console.log(filteredPosts)
return <LayoutBase {...props}>
Search {searchKey}
</LayoutBase>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

View File

@@ -0,0 +1,13 @@
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 LayoutBase from './LayoutBase'
export const LayoutSlug = (props) => {
return <LayoutBase {...props}>
首页
</LayoutBase>
}

View File

@@ -0,0 +1,7 @@
import LayoutBase from './LayoutBase'
export const LayoutTag = (props) => {
return <LayoutBase {...props}>
Tag - {props.tag}
</LayoutBase>
}

View File

@@ -0,0 +1,7 @@
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = (props) => {
return <LayoutBase {...props}>
Tag - {props.tag}
</LayoutBase>
}

View File

@@ -0,0 +1,47 @@
import BLOG from '@/blog.config'
import Logo from './Logo'
import GroupCategory from './GroupCategory'
import GroupMenu from './GroupMenu'
import GroupTag from './GroupTag'
import SearchInput from './SearchInput'
import SiteInfo from './SiteInfo'
function AsideLeft ({ tags, currentTag, categories, currentCategory }) {
return <div className='w-72 bg-white min-h-screen px-10 py-14'>
<Logo />
<section className='flex flex-col text-gray-600'>
<hr className='w-12 my-8' />
<GroupMenu/>
</section>
<section className='flex flex-col text-gray-600'>
<hr className='w-12 my-8' />
<SearchInput/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
{ BLOG.description }
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<GroupTag tags={tags} currentTag={currentTag}/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<GroupCategory categories={categories} currentCategory={currentCategory}/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<SiteInfo/>
</section>
</div>
}
export default AsideLeft

View File

@@ -0,0 +1,39 @@
import BLOG from '@/blog.config'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import CONFIG_FUKA from '../config_fuka'
import Card from './Card'
const BlogCard = ({ post, showSummary }) => {
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
return (
<Card className='w-full max-w-sm p-2'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
<div className='p-2 flex flex-col w-full'>
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<a className={`cursor-pointer font-bold hover:underline text-xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
{post.title}
</a>
</Link>
{(!showPreview || showSummary) && <p className='mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden'>
{post.summary}
</p>}
</div>
{CONFIG_FUKA.POST_LIST_COVER && post?.page_cover && (
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<div className='h-40 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
</div>
</Link>
)}
</div >
</Card>
)
}
export default BlogCard

View File

@@ -0,0 +1,12 @@
/**
* 空白博客 列表
* @returns {JSX.Element}
* @constructor
*/
const BlogListEmpty = ({ currentSearch }) => {
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
</div>
}
export default BlogListEmpty

View File

@@ -0,0 +1,36 @@
import BlogCard from './BlogCard'
import PaginationNumber from './PaginationNumber'
import BLOG from '@/blog.config'
import BlogPostListEmpty from './BlogListEmpty'
/**
* 文章列表分页表格
* @param page 当前页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogListPage = ({ page = 1, posts = [], postCount }) => {
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />
} else {
return (
<div id="container">
{/* 文章列表 */}
<div className="grid 2xl:grid-cols-3 lg:grid-cols-2 md:grid-cols-1 ">
{posts.map(post => (
<div key={post.id} className='justify-center flex'>
<BlogCard key={post.id} post={post} />
</div>
))}
</div>
<PaginationNumber page={page} totalPage={totalPage} />
</div>
)
}
}
export default BlogListPage

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
<>{headerSlot}</>
<section className="shadow px-2 py-4 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
{children}
</section>
</div>
}
export default Card

View File

@@ -0,0 +1,24 @@
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
function GroupCategory ({ currentCategory, categories }) {
return <>
<div id='category-list' className='dark:border-gray-600 flex flex-wrap'>
{Object.keys(categories).map(category => {
const selected = currentCategory === category
return <Link key={category} href={`/category/${category}`} passHref>
<a className={(selected
? 'hover:text-white dark:hover:text-white bg-gray-600 text-white '
: 'dark:text-gray-400 text-gray-500 hover:text-white hover:bg-gray-500 dark:hover:text-white') +
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
<FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={`${selected ? 'text-white' : 'text-gray-400'} mr-2`} />{category}({categories[category]})
</a>
</Link>
})}
</div>
</>
}
export default GroupCategory

View File

@@ -0,0 +1,38 @@
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import CONFIG_FUKA from '../config_fuka'
function GroupMenu () {
const { locale } = useGlobal()
const router = useRouter()
const links = [
{ id: 0, name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ id: 1, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_FUKA.MENU_CATEGORY },
{ id: 2, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_FUKA.MENU_TAG },
{ id: 3, name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_FUKA.MENU_ARCHIVE },
{ id: 4, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_FUKA.MENU_ABOUT }
]
return <nav id='nav' className='font-sans text-sm'>
{links.map(link => {
if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
<a className={'py-0.5 duration-500 justify-between text-gray-500 hover:text-black cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'text-black' : ' ')} >
<div className='my-auto items-center justify-center flex '>
<div className={ 'text-gray-500 hover:text-black'}>{link.name}</div>
</div>
{link.slot}
</a>
</Link>
} else {
return null
}
})}
</nav>
}
export default GroupMenu

View File

@@ -0,0 +1,24 @@
import TagItemMini from './TagItemMini'
/**
* 标签组
* @param tags
* @param currentTag
* @returns {JSX.Element}
* @constructor
*/
function GroupTag ({ tags, currentTag }) {
if (!tags) return <></>
return (
<div id='tags-group' className='dark:border-gray-600 w-66 space-y-2'>
{
tags.map(tag => {
const selected = tag.name === currentTag
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
})
}
</div>
)
}
export default GroupTag

View File

@@ -0,0 +1,12 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
function Logo () {
return <section className='flex'>
<Link href='/'>
<a className='hover:bg-black hover:text-white border-black border-2 duration-500 px-4 py-2 cursor-pointer font-black'>{BLOG.title}</a>
</Link>
</section>
}
export default Logo

View File

@@ -0,0 +1,94 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 数字翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const PaginationNumber = ({ page, totalPage }) => {
const router = useRouter()
const currentPage = +page
const showNext = page !== totalPage
const pages = generatePages(page, currentPage, totalPage)
return (
<div className='my-5 flex justify-center items-end font-medium text-black hover:shadow-xl duration-500 bg-white dark:bg-gray-700 dark:text-gray-300 py-3 shadow space-x-2'>
{/* 上一页 */}
<Link
href={ {
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
} } passHref >
<div
rel='prev'
className={`${currentPage === 1 ? 'invisible' : 'block'} border-white dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}
>
<FontAwesomeIcon icon={faAngleLeft}/>
</div>
</Link>
{pages}
{/* 下一页 */}
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
<div
rel='next'
className={`${+showNext ? 'block' : 'invisible'} border-t-2 border-white dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}
>
<FontAwesomeIcon icon={faAngleRight}/>
</div>
</Link>
</div>
)
}
function getPageElement (page, currentPage) {
return <Link href={page === 1 ? '/' : `/page/${page}`} key={page} passHref>
<a className={(page + '' === currentPage + '' ? 'font-bold bg-gray-500 dark:bg-gray-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-gray-400 ') +
' border-white dark:border-gray-700 dark:hover:border-gray-400 cursor-pointer w-6 text-center font-light hover:font-bold'}>
{page}
</a>
</Link>
}
function generatePages (page, currentPage, totalPage) {
const pages = []
const groupCount = 7 // 最多显示页签数
if (totalPage <= groupCount) {
for (let i = 1; i <= totalPage; i++) {
pages.push(getPageElement(i, page))
}
} else {
pages.push(getPageElement(1, page))
const dynamicGroupCount = groupCount - 2
let startPage = currentPage - 2
if (startPage <= 1) {
startPage = 2
}
if (startPage + dynamicGroupCount > totalPage) {
startPage = totalPage - dynamicGroupCount
}
if (startPage > 2) {
pages.push(<div key={-1}>... </div>)
}
for (let i = 0; i < dynamicGroupCount; i++) {
if (startPage + i < totalPage) {
pages.push(getPageElement(startPage + i, page))
}
}
if (startPage + dynamicGroupCount < totalPage) {
pages.push(<div key={-2}>... </div>)
}
pages.push(getPageElement(totalPage, page))
}
return pages
}
export default PaginationNumber

View File

@@ -0,0 +1,68 @@
import { useRouter } from 'next/router'
import { useImperativeHandle, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
const [searchKey, setSearchKey] = useState(currentSearch || '')
const [onLoading, setLoadingState] = useState(false)
const router = useRouter()
const searchInputRef = useRef()
useImperativeHandle(cRef, () => {
return {
focus: () => {
searchInputRef?.current?.focus()
}
}
})
const handleSearch = (key) => {
if (key && key !== '') {
setLoadingState(true)
router.push({ pathname: '/search', query: { s: key } }).then(r => {
setLoadingState(false)
})
} else {
router.push({ pathname: '/' }).then(r => {
})
}
}
const handleKeyUp = (e) => {
if (e.keyCode === 13) { // 回车
handleSearch(searchInputRef.current.value)
} else if (e.keyCode === 27) { // ESC
cleanSearch()
}
}
const cleanSearch = () => {
searchInputRef.current.value = ''
setSearchKey('')
}
const updateSearchKey = (val) => {
setSearchKey(val)
}
return <div className='flex w-full bg-gray-100'>
<input
ref={searchInputRef}
type='text'
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
onKeyUp={handleKeyUp}
onChange={e => updateSearchKey(e.target.value)}
defaultValue={searchKey}
/>
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}>
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
</div>
{(searchKey && searchKey.length &&
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
</div>
)}
</div>
}
export default SearchInput

View File

@@ -0,0 +1,26 @@
import BLOG from '@/blog.config'
import { faEye, faShieldAlt, faUsers } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
function SiteInfo ({ title }) {
const d = new Date()
const currentYear = d.getFullYear()
const startYear = BLOG.since && BLOG.since !== currentYear && BLOG.since + '-'
return (
<footer
className='leading-6 justify-start w-full text-gray-400 text-xs font-sans'
>
<span> © {`${startYear}${currentYear}`} <span> <a href={BLOG.link} className='text-gray-500 dark:text-gray-300 '>{BLOG.author}</a>. <br /></span>
<span>Powered by <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span><br /></span>
{BLOG.beian && <><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.beian}</a><br/></>}
<span className='hidden busuanzi_container_site_pv'> <FontAwesomeIcon icon={faEye} /><span className='px-1 busuanzi_value_site_pv'> </span> </span>
<span className='pl-2 hidden busuanzi_container_site_uv'> <FontAwesomeIcon icon={faUsers} /> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
<br />
<h1>{title}</h1>
</footer>
)
}
export default SiteInfo

View File

@@ -0,0 +1,17 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => {
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-gray-900'
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}` }>
<div className='font-light dark:text-gray-400'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a>
</Link>
}
export default TagItemMini

View File

@@ -0,0 +1,12 @@
const FUKA_CONFIG = {
POST_LIST_COVER: true, // 文章列表显示图片封面
// 菜单
MENU_ABOUT: true, // 显示关于
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索
}
export default FUKA_CONFIG

10
themes/Fukasawa/index.js Normal file
View File

@@ -0,0 +1,10 @@
export { LayoutIndex } from './LayoutIndex'
export { LayoutSearch } from './LayoutSearch'
export { LayoutArchive } from './LayoutArchive'
export { LayoutSlug } from './LayoutSlug'
export { Layout404 } from './Layout404'
export { LayoutCategory } from './LayoutCategory'
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
export { LayoutPage } from './LayoutPage'
export { LayoutTag } from './LayoutTag'
export { LayoutTagIndex } from './LayoutTagIndex'

View File

@@ -2,5 +2,6 @@
* 直接将./NEXT 替换成对应的主题路径
*/
export * from './NEXT' // 切换主题
// export * from './Empty' // 切换主题
// export * from './NEXT' // 切换主题
export * from './Fukasawa'
// export * from './Empty'