Medium V2.0

This commit is contained in:
tangly1024
2022-02-12 17:09:00 +08:00
parent 6b9bc95600
commit c929d6fa02
17 changed files with 294 additions and 83 deletions

View File

@@ -5,7 +5,7 @@ import React, { useState } from 'react'
* @param {*} param0
* @returns
*/
const Tabs = ({ children }) => {
const Tabs = ({ className, children }) => {
if (!children) {
return <></>
}
@@ -18,7 +18,7 @@ const Tabs = ({ children }) => {
})
if (count === 1) {
return <section className='duration-200'>
return <section className={'duration-200 ' + className}>
{children}
</section>
}
@@ -29,7 +29,7 @@ const Tabs = ({ children }) => {
setCurrentTab(i)
}
return <div className='mb-5 bg-white dark:bg-gray-800 duration-200'>
return <div className={'mb-5 bg-white dark:bg-gray-800 duration-200 ' + className}>
<ul className='flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600'>
{children.map((item, index) => {
return <li key={index}

View File

@@ -2,9 +2,16 @@ import CommonHead from '@/components/CommonHead'
import React from 'react'
import Footer from './components/Footer'
import InfoCard from './components/InfoCard'
import LogoBar from './components/LogoBar'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import RevolverMaps from './components/RevolverMaps'
import CONFIG_MEDIUM from './config_medium'
import Tabs from '@/components/Tabs'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import TopNavBar from './components/TopNavBar'
import Link from 'next/link'
import SearchInput from './components/SearchInput'
import { useRouter } from 'next/router'
import BottomMenuBar from './components/BottomMenuBar'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -16,21 +23,44 @@ const LayoutBase = props => {
const { children, meta, showInfoCard = true, slotRight } = props
return (
<div id="container" className='bg-white w-full h-full min-h-screen justify-center'>
<CommonHead meta={meta}/>
<main id="wrapper" className='max-w-7xl w-full h-full mx-auto'>
<LogoBar/>
<div className='w-72 px-8 py-12 fixed top-24 hidden lg:block'>
{showInfoCard && <InfoCard/>}
<div id='container' className='bg-white w-full h-full min-h-screen justify-center'>
<CommonHead meta={meta} />
<main id='wrapper' className='flex justify-between w-full h-full mx-auto'>
{/* 桌面端左侧菜单 */}
<div className='w-20 border-r hidden lg:block pt-12'>
<section>
<Link href='/'>
<div className='text-center cursor-pointer hover:text-black'>
<FontAwesomeIcon icon={faHome} size='lg' color='gray' />
</div>
</Link>
</section>
</div>
<div className='lg:ml-72 max-w-3xl w-full px-5'>
<div className='w-full justify-center'>
{/* 移动端顶部菜单 */}
<TopNavBar className='block lg:hidden' />
<div className='px-5'>
{children}
</div>
</div>
<div className='w-72 px-8 py-6 fixed right-12 top-24 hidden lg:block'>
{ CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps/>}
{ slotRight }
{/* 桌面端右侧 */}
<div className='hidden xl:block border-l w-96'>
<Tabs className='py-14 px-6 sticky top-0'>
{slotRight && <div key='Article' className='mt-6'>{slotRight} </div>}
<div key='About'>
<SearchInput className='mt-6 mb-12' />
{showInfoCard && <InfoCard />}
{CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps />}
</div>
</Tabs>
</div>
</main>
{/* 移动端底部 */}
<BottomMenuBar className='block md:hidden'/>
<Footer/>
</div>
)

View File

@@ -2,7 +2,6 @@ import BlogPostListPage from './components/BlogPostListPage'
import LayoutBase from './LayoutBase'
export const LayoutIndex = (props) => {
// const { posts, tags, meta, categories, postCount, latestPosts } = props
return <LayoutBase {...props}>
<BlogPostListPage {...props}/>
</LayoutBase>

View File

@@ -2,8 +2,7 @@ import LayoutBase from './LayoutBase'
import BlogPostListPage from './components/BlogPostListPage'
export const LayoutPage = (props) => {
const { page, posts, postCount } = props
return <LayoutBase {...props}>
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
<BlogPostListPage {...props} />
</LayoutBase>
}

View File

@@ -1,31 +1,24 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
import BlogPostListPage from './components/BlogPostListPage'
import SearchInput from './components/SearchInput'
import { useGlobal } from '@/lib/global'
import TagGroups from './components/TagGroups'
import CategoryGroup from './components/CategoryGroup'
export const LayoutSearch = (props) => {
const { posts } = props
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = posts
}
console.log(filteredPosts)
const { locale } = useGlobal()
return <LayoutBase {...props}>
Search {searchKey}
<div className='py-12'>
<div className='pb-4 w-full'>
{locale.NAV.SEARCH}
</div>
<SearchInput/>
<TagGroups {...props}/>
<CategoryGroup {...props}/>
</div>
<BlogPostListPage {...props}/>
</LayoutBase>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

View File

@@ -56,22 +56,24 @@ export const LayoutSlug = (props) => {
}
})
return <LayoutBase {...props} meta={meta} showInfoCard={false} slotRight={<Catalog toc={post.toc}/>}>
<h1 className='text-4xl mt-12 font-sans'>{post?.title}</h1>
<Link href='/about' passHref>
<div className='flex py-3 items-center font-sans cursor-pointer'>
<Image
alt={BLOG.AUTHOR}
width={25}
height={25}
loading='lazy'
src='/avatar.jpg'
className='rounded-full'
/>
<div className='mr-3 ml-1 text-green-500'>{BLOG.AUTHOR}</div>
<div className='text-gray-500'>{date}</div>
</div>
</Link>
return <LayoutBase {...props} meta={meta} showInfoCard={true} slotRight={<Catalog toc={post.toc}/>}>
<h1 className='text-4xl pt-12 font-sans'>{post?.title}</h1>
<div className='flex py-4 items-center font-sans px-1'>
<Link href='/about' passHref>
<>
<Image
alt={BLOG.AUTHOR}
width={25}
height={25}
loading='lazy'
src='/avatar.jpg'
className='rounded-full cursor-pointer'
/>
<div className='mr-3 ml-1 text-green-500 cursor-pointer'>{BLOG.AUTHOR}</div>
</>
</Link>
<div className='text-gray-500'>{date}</div>
</div>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1 max-w-5xl'>
{post.blockMap && (

View File

@@ -2,6 +2,7 @@ import BlogPostCard from './BlogPostCard'
import BLOG from '@/blog.config'
import BlogPostListEmpty from './BlogPostListEmpty'
import PaginationSimple from './PaginationSimple'
import { useRouter } from 'next/router'
/**
* 文章列表分页表格
@@ -12,7 +13,17 @@ import PaginationSimple from './PaginationSimple'
* @constructor
*/
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
let filteredPosts = Object.assign(posts)
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
}
const filteredPostsCount = filteredPosts.length
const totalPage = Math.ceil(filteredPostsCount / BLOG.POSTS_PER_PAGE)
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />
@@ -20,7 +31,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
return (
<div id="container" className='w-full justify-center'>
{/* 文章列表 */}
{posts.map(post => (
{filteredPosts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}
<PaginationSimple page={page} totalPage={totalPage} />
@@ -29,4 +40,12 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
}
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}
export default BlogPostListPage

View File

@@ -0,0 +1,23 @@
import { faHome, faSearch } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
export default function BottomMenuBar ({ className }) {
return (
<div className={'fixed bottom-0 w-full h-12 bg-white ' + className}>
<div className='flex justify-between h-full shadow-card'>
<Link href='/' passHref>
<div className='flex w-full items-center justify-center cursor-pointer'>
<FontAwesomeIcon icon={faHome} />
</div>
</Link>
<Link href='/search' passHref>
<div className='flex w-full items-center justify-center cursor-pointer'>
<FontAwesomeIcon icon={faSearch} />
</div>
</Link>
</div>
</div>
)
}

View File

@@ -0,0 +1,30 @@
import { faFolder, faFolderOpen, faTag, faTh } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
const CategoryGroup = ({ currentCategory, categories }) => {
if (!categories) {
return <></>
}
return <div id='category-list' className='pt-4'>
<div className='mb-2'><FontAwesomeIcon icon={faTh} className='mr-2' />分类</div>
<div className='flex flex-wrap'>
{Object.keys(categories).map(category => {
const selected = currentCategory === category
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 dark:hover:text-white hover:bg-gray-600') +
' text-sm items-center duration-300 cursor-pointer py-1 font-light px-2 whitespace-nowrap'}>
<div><FontAwesomeIcon icon={selected ? faFolderOpen : faFolder}
className={'mr-2'} />{category}({categories[category]})
</div>
</a>
</Link>
})}
</div>
</div>
}
export default CategoryGroup

View File

@@ -5,9 +5,9 @@ import React from 'react'
import SocialButton from './SocialButton'
const InfoCard = () => {
return <>
<div className='items-center justify-start font-sans '>
<div className='hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/about') }}>
return <div id='info-card' className='py-4'>
<div className='items-center justify-center font-sans '>
<div className='hover:scale-105 transform duration-200 cursor-pointer flex justify-center' onClick={ () => { Router.push('/about') }}>
<Image
alt={BLOG.AUTHOR}
width={120}
@@ -17,11 +17,11 @@ const InfoCard = () => {
className='rounded-full'
/>
</div>
<div className='text-xl py-2 hover:scale-105 transform duration-200'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200'>{BLOG.BIO}</div>
<div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.BIO}</div>
<SocialButton/>
</div>
</>
</div>
}
export default InfoCard

View File

@@ -1,25 +1,26 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
export default function LogoBar () {
const { locale } = useGlobal()
// const { locale } = useGlobal()
return <div id='top-wrapper' className='w-full flex justify-center font-sans'>
<div className='flex mx-auto w-full items-center space-x-3 py-6 px-5'>
<Link href='/'>
<a className='text-3xl'>{BLOG.TITLE}</a>
</Link>
<Link href='/about'>
<a className='text-gray-600'>{locale.NAV.ABOUT}</a>
</Link>
{BLOG.CONTACT_EMAIL && <Link href={`mailto:${BLOG.CONTACT_EMAIL}`} passHref>
<div className='bg-black px-2 py-1 rounded-full'>
<FontAwesomeIcon className='cursor-pointer text-white' icon={faEnvelope}/>
</div>
</Link>}
<div className='flex mx-auto w-full justify-between '>
<div className='space-x-3 flex items-center'>
<Link href='/'>
<a className='text-2xl'>{BLOG.TITLE}</a>
</Link>
{/* <Link href='/about'> */}
{/* <a className='text-gray-600'>{locale.NAV.ABOUT}</a> */}
{/* </Link> */}
</div>
{BLOG.CONTACT_EMAIL && <Link href={`mailto:${BLOG.CONTACT_EMAIL}`} passHref>
<div className='bg-black px-2 py-1 rounded-full'>
<FontAwesomeIcon className='cursor-pointer text-white' icon={faEnvelope} />
</div>
</Link>}
</div>
</div>
}

View File

@@ -8,7 +8,7 @@ export default function RevolverMaps () {
changeLoad(true)
}
})
return <div id="revolvermaps"/>
return <div id="revolvermaps" className='p-4'/>
}
function initRevolverMaps () {

View File

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

View File

@@ -10,7 +10,7 @@ import React from 'react'
* @constructor
*/
const SocialButton = () => {
return <div className='w-52 flex-wrap flex'>
return <div className='w-52 flex-wrap flex justify-center'>
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 '>
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
<FontAwesomeIcon icon={faGithub} className='transform hover:scale-125 duration-150'/>

View File

@@ -0,0 +1,29 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import TagItemMini from './TagItemMini'
/**
* 标签组
* @param tags
* @param currentTag
* @returns {JSX.Element}
* @constructor
*/
const TagGroups = ({ tags, currentTag }) => {
if (!tags) return <></>
return (
<div id='tags-group' className='dark:border-gray-600 py-4'>
<div className='mb-2'><FontAwesomeIcon icon={faTag} className='mr-2' />标签</div>
<div className='space-y-2'>
{
tags.map(tag => {
const selected = tag.name === currentTag
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
})
}
</div>
</div>
)
}
export default TagGroups

View File

@@ -5,7 +5,7 @@ import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => {
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white
${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-gray-900'
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}` }>

View File

@@ -0,0 +1,9 @@
import LogoBar from '@/themes/Medium/components/LogoBar'
export default function TopNavBar ({ className }) {
return <div id='top-nav' className={'sticky top-0 w-full z-50 ' + className}>
<div className='flex w-full h-12 shadow bg-white px-5 items-center'>
<LogoBar />
</div>
</div>
}