mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-19 07:26:50 +00:00
移动端适配
This commit is contained in:
@@ -1,7 +1,61 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
import BlogArchiveItem from './components/BlogPostArchive'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutArchive = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
Archive Page
|
||||
const { locale } = useGlobal()
|
||||
const { posts } = props
|
||||
// 深拷贝
|
||||
const postsSortByDate = Object.create(posts)
|
||||
|
||||
// 时间排序
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date.start_date.slice(0, 7)
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
archivePosts[date] = [post]
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogArchiveItem
|
||||
key={archiveTitle}
|
||||
posts={archivePosts[archiveTitle]}
|
||||
archiveTitle={archiveTitle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import TopNav from './components/TopNav'
|
||||
import AsideLeft from './components/AsideLeft'
|
||||
|
||||
/**
|
||||
@@ -15,27 +16,23 @@ import AsideLeft from './components/AsideLeft'
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = ({
|
||||
children,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
post,
|
||||
postCount,
|
||||
sideBarSlot,
|
||||
floatSlot,
|
||||
rightAreaSlot,
|
||||
currentSearch,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories
|
||||
}) => {
|
||||
const LayoutBase = (props) => {
|
||||
const {
|
||||
children,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories
|
||||
} = props
|
||||
return (<>
|
||||
<CommonHead meta={meta} />
|
||||
<div className='flex flex-between'>
|
||||
<TopNav {...props}/>
|
||||
<div className='flex'>
|
||||
<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'>
|
||||
<main id='wrapper' className='flex w-full py-8 justify-center'>
|
||||
<div className='2xl:max-w-6xl md:max-w-3xl w-full'>
|
||||
<div> {headerSlot} </div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategory = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
Category
|
||||
<BlogListPage page={props.page} posts={props.posts} postCount={props.postCount} />
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faFolder, faTh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategoryIndex = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
Category
|
||||
</LayoutBase>
|
||||
const { locale } = useGlobal()
|
||||
const { categories } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'>
|
||||
<FontAwesomeIcon icon={faTh} className='mr-4' />{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{Object.keys(categories).map(category => {
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<div
|
||||
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})
|
||||
</div>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</div> </LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTag = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
Tag - {props.tag}
|
||||
<BlogListPage {...props} />
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import TagItem from './components/TagItem'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTagIndex = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
Tag - {props.tag}
|
||||
const { locale } = useGlobal()
|
||||
const { tags } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTag} className='mr-4'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{ tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ export default function ArticleAround ({ prev, next }) {
|
||||
}
|
||||
return <section className='text-gray-800 h-28 flex items-center justify-between space-x-5 my-4'>
|
||||
<Link href={`/article/${prev.slug}`} passHref>
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-50 hover:bg-gray-700 hover:text-white duration-300'>
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/article/${next.slug}`} passHref>
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-50 hover:bg-gray-700 hover:text-white duration-300'>{next.title}
|
||||
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>{next.title}
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
|
||||
</a>
|
||||
</Link>
|
||||
@@ -13,7 +13,7 @@ import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import ArticleAround from './ArticleArount'
|
||||
import ArticleAround from './ArticleAround'
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -42,17 +42,18 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
}
|
||||
})
|
||||
|
||||
return (<div id="container" className=" max-w-4xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
</div>
|
||||
)}
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-32 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
|
||||
<header className='animate__slideInDown animate__animated'>
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
|
||||
|
||||
@@ -7,7 +7,7 @@ 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'>
|
||||
return <div className='w-72 bg-white min-h-screen px-10 py-14 hidden lg:block'>
|
||||
|
||||
<Logo />
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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'>
|
||||
<Card className='w-full lg: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'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import BlogCard from './BlogCard'
|
||||
import BlogPostListEmpty from './BlogListEmpty'
|
||||
import PaginationSimple from './PaginationSimple'
|
||||
@@ -13,14 +14,32 @@ import PaginationSimple from './PaginationSimple'
|
||||
*/
|
||||
const BlogListPage = ({ page = 1, posts = [], postCount }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
|
||||
const showNext = page < totalPage
|
||||
const showNext = page < totalPage && posts.length < postCount
|
||||
const [colCount, changeCol] = useState(3)
|
||||
|
||||
function updateCol () {
|
||||
if (window.outerWidth > 1200) {
|
||||
changeCol(3)
|
||||
} else {
|
||||
changeCol(1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateCol()
|
||||
window.addEventListener('resize', updateCol)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateCol)
|
||||
}
|
||||
})
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container">
|
||||
{/* 文章列表 */}
|
||||
<div style={{ columnCount: 3 }}>
|
||||
<div style={{ columnCount: colCount }}>
|
||||
{posts.map(post => (
|
||||
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard key={post.id} post={post} />
|
||||
|
||||
32
themes/Fukasawa/components/BlogPostArchive.js
Normal file
32
themes/Fukasawa/components/BlogPostArchive.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
* 博客归档
|
||||
* @param posts 所有文章
|
||||
* @param archiveTitle 归档标题
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogArchiveItem = ({ posts = [], archiveTitle }) => {
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return <div>
|
||||
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
|
||||
<ul>
|
||||
{posts.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 name={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className='dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600'>{post.title}</a>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogArchiveItem
|
||||
38
themes/Fukasawa/components/Collapse.js
Normal file
38
themes/Fukasawa/components/Collapse.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
const Collapse = props => {
|
||||
const collapseRef = useRef(null)
|
||||
const collapseSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = sectionHeight + 'px'
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = 0 + 'px'
|
||||
})
|
||||
})
|
||||
}
|
||||
const expandSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
element.style.height = sectionHeight + 'px'
|
||||
const clearTime = setTimeout(() => {
|
||||
element.style.height = 'auto'
|
||||
}, 400)
|
||||
clearTimeout(clearTime)
|
||||
}
|
||||
useEffect(() => {
|
||||
const element = collapseRef.current
|
||||
if (props.isOpen) {
|
||||
expandSection(element)
|
||||
} else {
|
||||
collapseSection(element)
|
||||
}
|
||||
}, [props.isOpen])
|
||||
return (
|
||||
<div ref={collapseRef} style={{ height: '0px' }} className='overflow-hidden duration-200'>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Collapse.defaultProps = { isOpen: false }
|
||||
|
||||
export default Collapse
|
||||
26
themes/Fukasawa/components/TagItem.js
Normal file
26
themes/Fukasawa/components/TagItem.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const TagItem = ({ tag, selected }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (!tag) {
|
||||
<div> { locale.COMMON.NOTAG } </div>
|
||||
}
|
||||
return (
|
||||
<Link href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<li
|
||||
className={`notion-${tag.color}_background dark:bg-gray-700 list-none cursor-pointer rounded-md
|
||||
duration-200 mr-1 my-1 px-2 py-1 text-sm whitespace-nowrap
|
||||
hover:bg-gray-200 dark:hover:bg-gray-800 `}>
|
||||
<div className='text-gray-600 dark:text-gray-300 dark:hover:text-white'>
|
||||
{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {`${tag.name} `} {tag.count ? `(${tag.count})` : ''}
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagItem
|
||||
51
themes/Fukasawa/components/TopNav.js
Normal file
51
themes/Fukasawa/components/TopNav.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useState } from 'react'
|
||||
import Collapse from './Collapse'
|
||||
import GroupMenu from './GroupMenu'
|
||||
import Logo from './Logo'
|
||||
|
||||
/**
|
||||
* 顶部导航
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => {
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
return (<div id='top-nav' className='z-40 block lg:hidden'>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'lg:relative w-full top-0 z-20 transform duration-500'}>
|
||||
<Collapse isOpen={isOpen}>
|
||||
<div className='bg-white py-1 px-5'>
|
||||
<GroupMenu/>
|
||||
</div>
|
||||
</Collapse>
|
||||
<div className='w-full flex justify-between items-center p-4 bg-white'>
|
||||
{/* 左侧LOGO 标题 */}
|
||||
<div className='flex flex-none flex-grow-0'>
|
||||
<Logo/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='flex'>
|
||||
</div>
|
||||
|
||||
{/* 右侧功能 */}
|
||||
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
|
||||
<div onClick={toggleMenuOpen} className='w-18 cursor-pointer'>
|
||||
菜单 { isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
@@ -9,11 +9,10 @@ import BLOG from '@/blog.config'
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
|
||||
const targetRef = useRef(null)
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return <div ref={targetRef}>
|
||||
return <div>
|
||||
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
|
||||
Reference in New Issue
Block a user