mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-06-09 07:26:47 +00:00
theme-movie
This commit is contained in:
@@ -17,105 +17,126 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
const currentPage = +page
|
const currentPage = +page
|
||||||
const showNext = page < totalPage
|
const showNext = page < totalPage
|
||||||
const showPrev = currentPage !== 1
|
const showPrev = currentPage !== 1
|
||||||
const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
const pagePrefix = router.asPath
|
||||||
|
.split('?')[0]
|
||||||
|
.replace(/\/page\/[1-9]\d*/, '')
|
||||||
|
.replace(/\/$/, '')
|
||||||
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
||||||
|
|
||||||
const [value, setValue] = useState('')
|
const [value, setValue] = useState('')
|
||||||
|
|
||||||
const handleInputChange = (event) => {
|
const handleInputChange = event => {
|
||||||
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调到指定页
|
* 调到指定页
|
||||||
*/
|
*/
|
||||||
const jumpToPage = () => {
|
const jumpToPage = () => {
|
||||||
if (value) {
|
if (value) {
|
||||||
router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`)
|
router.push(
|
||||||
|
value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
|
<>
|
||||||
{/* pc端分页按钮 */}
|
{/* pc端分页按钮 */}
|
||||||
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
||||||
{/* 上一页 */}
|
{/* 上一页 */}
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
||||||
pathname: currentPage === 2 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`,
|
pathname:
|
||||||
query: router.query.s ? { s: router.query.s } : {}
|
currentPage === 2
|
||||||
}}
|
? `${pagePrefix}/`
|
||||||
rel="prev"
|
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||||
className={`${currentPage === 1 ? 'invisible' : 'block'}`}>
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
}}
|
||||||
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
rel="prev"
|
||||||
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
className={`${currentPage === 1 ? 'invisible' : 'block'}`}
|
||||||
{locale.PAGINATION.PREV}
|
>
|
||||||
</div>
|
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||||
</div>
|
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
||||||
|
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
||||||
</Link>
|
{locale.PAGINATION.PREV}
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
<div className='flex items-center space-x-2'>
|
|
||||||
{pages}
|
|
||||||
|
|
||||||
{/* 跳转页码 */}
|
|
||||||
<div className='bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200'>
|
|
||||||
<input value={value} className='w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg' onInput={handleInputChange}></input>
|
|
||||||
<div onClick={jumpToPage} className='cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white'>
|
|
||||||
<ChevronDoubleRight className={'w-4 h-4'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
{/* 下一页 */}
|
{/* 分页 */}
|
||||||
<Link
|
<div className="flex items-center space-x-2">
|
||||||
href={{
|
{pages}
|
||||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
|
||||||
query: router.query.s ? { s: router.query.s } : {}
|
|
||||||
}}
|
|
||||||
rel="next"
|
|
||||||
className={`${+showNext ? 'block' : 'invisible'} `}>
|
|
||||||
|
|
||||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
{/* 跳转页码 */}
|
||||||
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
<div className="bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200">
|
||||||
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
<input
|
||||||
{locale.PAGINATION.NEXT}
|
value={value}
|
||||||
</div>
|
className="w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg"
|
||||||
</div>
|
onInput={handleInputChange}
|
||||||
</Link>
|
></input>
|
||||||
|
<div
|
||||||
|
onClick={jumpToPage}
|
||||||
|
className="cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white"
|
||||||
|
>
|
||||||
|
<ChevronDoubleRight className={'w-4 h-4'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端分页 */}
|
{/* 下一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="next"
|
||||||
|
className={`${+showNext ? 'block' : 'invisible'} `}
|
||||||
|
>
|
||||||
|
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||||
|
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
||||||
|
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
||||||
|
{locale.PAGINATION.NEXT}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='lg:hidden w-full flex flex-row'>
|
{/* 移动端分页 */}
|
||||||
{/* 上一页 */}
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: currentPage === 2 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`,
|
|
||||||
query: router.query.s ? { s: router.query.s } : {}
|
|
||||||
}}
|
|
||||||
rel="prev"
|
|
||||||
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}>
|
|
||||||
{locale.PAGINATION.PREV}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{showPrev && showNext && <div className='w-12'></div>}
|
<div className="lg:hidden w-full flex flex-row">
|
||||||
|
{/* 上一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname:
|
||||||
|
currentPage === 2
|
||||||
|
? `${pagePrefix}/`
|
||||||
|
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="prev"
|
||||||
|
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||||
|
>
|
||||||
|
{locale.PAGINATION.PREV}
|
||||||
|
</Link>
|
||||||
|
|
||||||
{/* 下一页 */}
|
{showPrev && showNext && <div className="w-12"></div>}
|
||||||
<Link
|
|
||||||
href={{
|
{/* 下一页 */}
|
||||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
<Link
|
||||||
query: router.query.s ? { s: router.query.s } : {}
|
href={{
|
||||||
}}
|
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||||
rel="next"
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}>
|
}}
|
||||||
{locale.PAGINATION.NEXT}
|
rel="next"
|
||||||
</Link>
|
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||||
</div>
|
>
|
||||||
</>)
|
{locale.PAGINATION.NEXT}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,21 +148,23 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
*/
|
*/
|
||||||
function getPageElement(page, currentPage, pagePrefix) {
|
function getPageElement(page, currentPage, pagePrefix) {
|
||||||
const selected = page + '' === currentPage + ''
|
const selected = page + '' === currentPage + ''
|
||||||
|
if (!page) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(<Link
|
<Link
|
||||||
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
||||||
key={page}
|
key={page}
|
||||||
passHref
|
passHref
|
||||||
className={
|
className={
|
||||||
(selected
|
(selected
|
||||||
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
||||||
: 'dark:bg-[#1e1e1e] bg-white') +
|
: 'dark:bg-[#1e1e1e] bg-white') +
|
||||||
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
{page}
|
{page}
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +194,11 @@ function generatePages(pagePrefix, page, currentPage, totalPage) {
|
|||||||
startPage = totalPage - dynamicGroupCount
|
startPage = totalPage - dynamicGroupCount
|
||||||
}
|
}
|
||||||
if (startPage > 2) {
|
if (startPage > 2) {
|
||||||
pages.push(<div key={-1} className='-mt-2 mx-1'>... </div>)
|
pages.push(
|
||||||
|
<div key={-1} className="-mt-2 mx-1">
|
||||||
|
...{' '}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < dynamicGroupCount; i++) {
|
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||||
|
|||||||
22
themes/movie/components/Announcement.js
Normal file
22
themes/movie/components/Announcement.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
const NotionPage = dynamic(() => import('@/components/NotionPage'))
|
||||||
|
|
||||||
|
const Announcement = ({ post, className }) => {
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
if (!post || Object.keys(post).length === 0) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return <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">
|
||||||
|
<i className="mr-2 fas fa-bullhorn" />{locale.COMMON.ANNOUNCEMENT}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{post && (<div id="announcement-content">
|
||||||
|
<NotionPage post={post} className='text-center ' />
|
||||||
|
</div>)}
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
export default Announcement
|
||||||
51
themes/movie/components/ArticleInfo.js
Normal file
51
themes/movie/components/ArticleInfo.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import { formatDateFmt } from '@/lib/utils/formatDate'
|
||||||
|
|
||||||
|
export const ArticleInfo = (props) => {
|
||||||
|
const { post } = props
|
||||||
|
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||||
|
<div>
|
||||||
|
{post?.type !== 'Page' && <>
|
||||||
|
<Link
|
||||||
|
href={`/category/${post?.category}`}
|
||||||
|
passHref
|
||||||
|
className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||||
|
|
||||||
|
<i className="mr-1 fas fa-folder-open" />
|
||||||
|
{post?.category}
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
<span className='mr-2'>|</span>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
{post?.type !== 'Page' && (<>
|
||||||
|
<Link
|
||||||
|
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}
|
||||||
|
passHref
|
||||||
|
className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
|
||||||
|
|
||||||
|
{post?.publishDay}
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
<span className='mr-2'>|</span>
|
||||||
|
<span className='mx-2 text-gray-400 dark:text-gray-500'>
|
||||||
|
{locale.COMMON.LAST_EDITED_TIME}: {post?.lastEditedDay}
|
||||||
|
</span>
|
||||||
|
<span className='mr-2'>|</span>
|
||||||
|
<span className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||||
|
<i className='mr-1 fas fa-eye' />
|
||||||
|
|
||||||
|
<span className="mr-2 busuanzi_value_page_pv" />
|
||||||
|
</span>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
themes/movie/components/ArticleLock.js
Normal file
52
themes/movie/components/ArticleLock.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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 '>
|
||||||
|
<div className='text-center space-y-3'>
|
||||||
|
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||||
|
<div className='flex mx-4'>
|
||||||
|
<input id="password" type='password'
|
||||||
|
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>
|
||||||
|
}
|
||||||
38
themes/movie/components/BlogListGroupByDate.js
Normal file
38
themes/movie/components/BlogListGroupByDate.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按照日期将文章分组
|
||||||
|
* 归档页面用到
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function BlogListGroupByDate({ 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 => {
|
||||||
|
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||||
|
|
||||||
|
return <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?.publishDay}>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{post?.publishDay}
|
||||||
|
</span>{' '}
|
||||||
|
|
||||||
|
<Link href={url} 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>
|
||||||
|
}
|
||||||
41
themes/movie/components/BlogListPage.js
Normal file
41
themes/movie/components/BlogListPage.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
import BlogPostCard from './BlogPostCard'
|
||||||
|
import PaginationNumber from './PaginationNumber'
|
||||||
|
|
||||||
|
export const BlogListPage = props => {
|
||||||
|
const { page = 1, posts, postCount } = props
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
const router = useRouter()
|
||||||
|
const totalPage = Math.ceil(
|
||||||
|
postCount / parseInt(siteConfig('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(/\/$/, '')
|
||||||
|
|
||||||
|
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full ${showPageCover ? 'md:pr-2' : 'md:pr-12'} mb-12`}>
|
||||||
|
<div
|
||||||
|
id="posts-wrapper"
|
||||||
|
className="grid md:grid-cols-2 md:gap-12 lg:grid-cols-3 lg:gap-20 xl:gap-24 2xl:grid-cols-4"
|
||||||
|
>
|
||||||
|
{posts?.map(post => (
|
||||||
|
<BlogPostCard key={post.id} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PaginationNumber {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
themes/movie/components/BlogListScroll.js
Normal file
66
themes/movie/components/BlogListScroll.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
import BlogPostCard from './BlogPostCard'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
|
||||||
|
export const BlogListScroll = props => {
|
||||||
|
const { posts } = props
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
|
const [page, updatePage] = useState(1)
|
||||||
|
|
||||||
|
let hasMore = false
|
||||||
|
const postsToShow = posts
|
||||||
|
? Object.assign(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (posts) {
|
||||||
|
const totalCount = posts.length
|
||||||
|
hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount
|
||||||
|
}
|
||||||
|
const handleGetMore = () => {
|
||||||
|
if (!hasMore) return
|
||||||
|
updatePage(page + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRef = useRef(null)
|
||||||
|
|
||||||
|
// 监听滚动自动分页加载
|
||||||
|
const scrollTrigger = useCallback(throttle(() => {
|
||||||
|
const scrollS = window.scrollY + window.outerHeight
|
||||||
|
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||||
|
if (scrollS > clientHeight + 100) {
|
||||||
|
handleGetMore()
|
||||||
|
}
|
||||||
|
}, 500))
|
||||||
|
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('scroll', scrollTrigger)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', scrollTrigger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div id='posts-wrapper' className={`w-full ${showPageCover ? 'md:pr-2' : 'md:pr-12'}} mb-12`} ref={targetRef}>
|
||||||
|
|
||||||
|
{postsToShow?.map(post => (
|
||||||
|
<BlogPostCard key={post.id} post={post} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={handleGetMore}
|
||||||
|
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
themes/movie/components/BlogPostCard.js
Normal file
62
themes/movie/components/BlogPostCard.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import TagItemMini from './TagItemMini'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
import TwikooCommentCount from '@/components/TwikooCommentCount'
|
||||||
|
import LazyImage from '@/components/LazyImage'
|
||||||
|
import { formatDateFmt } from '@/lib/utils/formatDate'
|
||||||
|
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||||
|
import NotionIcon from '@/components/NotionIcon'
|
||||||
|
|
||||||
|
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
|
||||||
|
// 主题默认强制显示图片
|
||||||
|
if (post && !post.pageCoverThumbnail) {
|
||||||
|
post.pageCoverThumbnail =
|
||||||
|
siteInfo?.pageCover || siteConfig('RANDOM_IMAGE_URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = checkContainHttp(post.slug)
|
||||||
|
? sliceUrlFromHttp(post.slug)
|
||||||
|
: `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="w-full mb-4 cursor-pointer overflow-hidden shadow-movie dark:bg-hexo-black-gray">
|
||||||
|
<Link href={url} passHref legacyBehavior>
|
||||||
|
{/* 固定高度 ,空白用图片拉升填充 */}
|
||||||
|
<div className="group flex flex-col aspect-[2/3] justify-between relative">
|
||||||
|
{/* 图片 填充卡片 */}
|
||||||
|
<div className="flex flex-grow w-full h-full relative duration-200 cursor-pointer transform overflow-hidden">
|
||||||
|
<LazyImage
|
||||||
|
src={post?.pageCoverThumbnail}
|
||||||
|
alt={post.title}
|
||||||
|
className="h-full w-full group-hover:scale-105 transform object-cover duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-32 z-20">
|
||||||
|
{post?.tagItems && post?.tagItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-6 justify-between flex p-2">
|
||||||
|
{post.tagItems.map(tag => (
|
||||||
|
<TagItemMini key={tag.name} tag={tag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* 阴影遮罩 */}
|
||||||
|
<h2 className="absolute bottom-10 px-6 transition-all duration-200 text-white text-2xl font-semibold break-words shadow-text z-20">
|
||||||
|
<NotionIcon icon={post.pageIcon} />
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="h-3/4 w-full absolute left-0 bottom-0 z-10">
|
||||||
|
<div className="h-full w-full absolute opacity-80 group-hover:opacity-100 transition-all duration-1000 bg-gradient-to-b from-transparent to-black"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogPostCard
|
||||||
20
themes/movie/components/CategoryItem.js
Normal file
20
themes/movie/components/CategoryItem.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章分类
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function CategoryItem({ 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
themes/movie/components/ExampleRecentComments.js
Normal file
35
themes/movie/components/ExampleRecentComments.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { siteConfig } from '@/lib/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] = useState([])
|
||||||
|
const [onLoading, changeLoading] = useState(true)
|
||||||
|
useEffect(() => {
|
||||||
|
RecentComments({
|
||||||
|
serverURL: siteConfig('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 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
|
||||||
26
themes/movie/components/Footer.js
Normal file
26
themes/movie/components/Footer.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import DarkModeButton from '@/components/DarkModeButton'
|
||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
|
||||||
|
export const Footer = (props) => {
|
||||||
|
const d = new Date()
|
||||||
|
const currentYear = d.getFullYear()
|
||||||
|
const since = siteConfig('SINCE')
|
||||||
|
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
|
||||||
|
|
||||||
|
return <footer className="z-10 relative w-full bg-white px-6 border-t dark:border-hexo-black-gray dark:bg-hexo-black-gray ">
|
||||||
|
<DarkModeButton className='text-center pt-4'/>
|
||||||
|
|
||||||
|
<div className="container mx-auto max-w-4xl py-6 md:flex flex-wrap md:flex-no-wrap md:justify-between items-center text-sm">
|
||||||
|
<div className='text-center'> ©{`${copyrightDate}`} {siteConfig('AUTHOR')}. All rights reserved.</div>
|
||||||
|
<div className="md:p-0 text-center md:text-right text-xs">
|
||||||
|
{/* 右侧链接 */}
|
||||||
|
{/* <a href="#" className="text-black no-underline hover:underline">Privacy Policy</a> */}
|
||||||
|
{siteConfig('BEI_AN') && (<a href="https://beian.miit.gov.cn/" className="text-black dark:text-gray-200 no-underline hover:underline ml-4">{siteConfig('BEI_AN')} </a>)}
|
||||||
|
<span className='dark:text-gray-200 no-underline ml-4'>
|
||||||
|
Powered by
|
||||||
|
<a href="https://github.com/tangly1024/NotionNext" className=' hover:underline'> NotionNext {siteConfig('VERSION')} </a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
26
themes/movie/components/Header.js
Normal file
26
themes/movie/components/Header.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { MenuListTop } from './MenuListTop'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网站顶部
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Header = props => {
|
||||||
|
return (
|
||||||
|
<header className="w-full px-6 relative z-10">
|
||||||
|
<div className="mx-auto md:flex justify-between items-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="py-6 w-full text-3xl font-bold text-center md:text-left md:w-auto text-gray-dark no-underline flex justify-center items-center"
|
||||||
|
>
|
||||||
|
{siteConfig('TITLE')}
|
||||||
|
</Link>
|
||||||
|
<div className="w-full md:w-auto text-center md:text-right">
|
||||||
|
{/* 右侧文字 */}
|
||||||
|
<MenuListTop {...props} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
themes/movie/components/JumpToTopButton.js
Normal file
18
themes/movie/components/JumpToTopButton.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到网页顶部
|
||||||
|
* 当屏幕下滑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
|
||||||
8
themes/movie/components/LoadingCover.js
Normal file
8
themes/movie/components/LoadingCover.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
export default function LoadingCover() {
|
||||||
|
return <div id='cover-loading' className={'z-50 opacity-50 pointer-events-none transition-all duration-300'}>
|
||||||
|
<div className='w-full h-screen flex justify-center items-center'>
|
||||||
|
<i className="fa-solid fa-spinner text-2xl text-black dark:text-white animate-spin"> </i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
67
themes/movie/components/MenuItemDrop.js
Normal file
67
themes/movie/components/MenuItemDrop.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export const MenuItemDrop = ({ link }) => {
|
||||||
|
const [show, changeShow] = useState(false)
|
||||||
|
const hasSubMenu = link?.subMenus?.length > 0
|
||||||
|
|
||||||
|
if (!link || !link.show) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseOver={() => changeShow(true)}
|
||||||
|
onMouseOut={() => changeShow(false)}
|
||||||
|
>
|
||||||
|
{!hasSubMenu && (
|
||||||
|
<Link
|
||||||
|
href={link?.to}
|
||||||
|
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||||
|
className=" menu-link pl-2 pr-4 no-underline tracking-widest pb-1"
|
||||||
|
>
|
||||||
|
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||||
|
{hasSubMenu && <i className="px-2 fa fa-angle-down"></i>}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasSubMenu && (
|
||||||
|
<>
|
||||||
|
<div className="cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1">
|
||||||
|
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||||
|
<i
|
||||||
|
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 子菜单 */}
|
||||||
|
{hasSubMenu && (
|
||||||
|
<ul
|
||||||
|
style={{ backdropFilter: 'blur(3px)' }}
|
||||||
|
className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-30 absolute block `}
|
||||||
|
>
|
||||||
|
{link.subMenus.map((sLink, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="cursor-pointer hover:bg-indigo-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={sLink.to}
|
||||||
|
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-nowrap font-extralight">
|
||||||
|
{link?.icon && <i className={sLink?.icon}> </i>}
|
||||||
|
{sLink.title}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
themes/movie/components/MenuListTop.js
Normal file
42
themes/movie/components/MenuListTop.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { MenuItemDrop } from './MenuItemDrop'
|
||||||
|
|
||||||
|
export const MenuListTop = (props) => {
|
||||||
|
const { customNav, customMenu } = props
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
|
let links = [
|
||||||
|
{ id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: siteConfig('HEXO_MENU_INDEX', null, CONFIG) },
|
||||||
|
{ id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: siteConfig('HEXO_MENU_SEARCH', null, CONFIG) },
|
||||||
|
{ id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: siteConfig('HEXO_MENU_ARCHIVE', null, CONFIG) }
|
||||||
|
// { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: siteConfig('MENU_CATEGORY', null, CONFIG) },
|
||||||
|
// { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('MENU_TAG', null, CONFIG) }
|
||||||
|
]
|
||||||
|
|
||||||
|
if (customNav) {
|
||||||
|
links = links.concat(customNav)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
if (links[i].id !== i) {
|
||||||
|
links[i].id = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 开启自定义菜单,则覆盖Page生成的菜单
|
||||||
|
if (siteConfig('CUSTOM_MENU')) {
|
||||||
|
links = customMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!links || links.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<nav id='nav-mobile' className='leading-8 justify-center font-light w-full flex'>
|
||||||
|
{links?.map((link, index) => link && link.show && <MenuItemDrop key={index} link={link} />)}
|
||||||
|
</nav>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
73
themes/movie/components/Nav.js
Normal file
73
themes/movie/components/Nav.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
import { MenuItemDrop } from './MenuItemDrop'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单导航
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Nav = props => {
|
||||||
|
const { customNav, customMenu } = props
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
|
let links = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: 'fas fa-search',
|
||||||
|
name: locale.NAV.SEARCH,
|
||||||
|
to: '/search',
|
||||||
|
show: siteConfig('EXAMPLE_MENU_SEARCH', null, CONFIG)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: 'fas fa-archive',
|
||||||
|
name: locale.NAV.ARCHIVE,
|
||||||
|
to: '/archive',
|
||||||
|
show: siteConfig('EXAMPLE_MENU_ARCHIVE', null, CONFIG)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: 'fas fa-folder',
|
||||||
|
name: locale.COMMON.CATEGORY,
|
||||||
|
to: '/category',
|
||||||
|
show: siteConfig('EXAMPLE_MENU_CATEGORY', null, CONFIG)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: 'fas fa-tag',
|
||||||
|
name: locale.COMMON.TAGS,
|
||||||
|
to: '/tag',
|
||||||
|
show: siteConfig('EXAMPLE_MENU_TAG', null, CONFIG)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (customNav) {
|
||||||
|
links = links.concat(customNav)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 开启自定义菜单,则不再使用 Page生成菜单。
|
||||||
|
if (siteConfig('CUSTOM_MENU')) {
|
||||||
|
links = customMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!links || links.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="w-full bg-white md:pt-0 px-6 relative z-30 border-t border-b border-gray-light dark:border-hexo-black-gray dark:bg-black">
|
||||||
|
<div className="container mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start">
|
||||||
|
<ul className="w-full text-center md:text-left flex flex-wrap justify-center items-stretch md:justify-start md:items-start">
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<MenuItemDrop key={index} link={link} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{/* <div className="w-full md:w-1/3 text-center md:text-right"> */}
|
||||||
|
{/* <!-- extra links --> */}
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
themes/movie/components/NormalMenuItem.js
Normal file
14
themes/movie/components/NormalMenuItem.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧的普通菜单
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const NormalMenuItem = (props) => {
|
||||||
|
const { link } = props
|
||||||
|
return link?.show && <Link href={link.to} key={link.to}
|
||||||
|
className="px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light">
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
218
themes/movie/components/PaginationNumber.js
Normal file
218
themes/movie/components/PaginationNumber.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { ChevronDoubleRight } from '@/components/HeroIcons'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字翻页插件
|
||||||
|
* @param page 当前页码
|
||||||
|
* @param showNext 是否有下一页
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const PaginationNumber = ({ page, totalPage }) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
const currentPage = +page
|
||||||
|
const showNext = page < totalPage
|
||||||
|
const showPrev = currentPage !== 1
|
||||||
|
const pagePrefix = router.asPath
|
||||||
|
.split('?')[0]
|
||||||
|
.replace(/\/page\/[1-9]\d*/, '')
|
||||||
|
.replace(/\/$/, '')
|
||||||
|
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
||||||
|
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
|
||||||
|
const handleInputChange = event => {
|
||||||
|
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调到指定页
|
||||||
|
*/
|
||||||
|
const jumpToPage = () => {
|
||||||
|
if (value) {
|
||||||
|
router.push(
|
||||||
|
value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* pc端分页按钮 */}
|
||||||
|
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
||||||
|
{/* 上一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname:
|
||||||
|
currentPage === 2
|
||||||
|
? `${pagePrefix}/`
|
||||||
|
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="prev"
|
||||||
|
className={`${currentPage === 1 ? 'invisible' : 'block'}`}
|
||||||
|
>
|
||||||
|
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||||
|
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
||||||
|
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
||||||
|
{locale.PAGINATION.PREV}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{pages}
|
||||||
|
|
||||||
|
{/* 跳转页码 */}
|
||||||
|
<div className="bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200">
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
className="w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg"
|
||||||
|
onInput={handleInputChange}
|
||||||
|
></input>
|
||||||
|
<div
|
||||||
|
onClick={jumpToPage}
|
||||||
|
className="cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white"
|
||||||
|
>
|
||||||
|
<ChevronDoubleRight className={'w-4 h-4'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="next"
|
||||||
|
className={`${+showNext ? 'block' : 'invisible'} `}
|
||||||
|
>
|
||||||
|
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||||
|
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
||||||
|
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
||||||
|
{locale.PAGINATION.NEXT}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端分页 */}
|
||||||
|
|
||||||
|
<div className="lg:hidden w-full flex flex-row">
|
||||||
|
{/* 上一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname:
|
||||||
|
currentPage === 2
|
||||||
|
? `${pagePrefix}/`
|
||||||
|
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="prev"
|
||||||
|
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||||
|
>
|
||||||
|
{locale.PAGINATION.PREV}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{showPrev && showNext && <div className="w-12"></div>}
|
||||||
|
|
||||||
|
{/* 下一页 */}
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||||
|
query: router.query.s ? { s: router.query.s } : {}
|
||||||
|
}}
|
||||||
|
rel="next"
|
||||||
|
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||||
|
>
|
||||||
|
{locale.PAGINATION.NEXT}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码按钮
|
||||||
|
* @param {*} page
|
||||||
|
* @param {*} currentPage
|
||||||
|
* @param {*} pagePrefix
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getPageElement(page, currentPage, pagePrefix) {
|
||||||
|
const selected = page + '' === currentPage + ''
|
||||||
|
if (!page) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
||||||
|
key={page}
|
||||||
|
passHref
|
||||||
|
className={
|
||||||
|
(selected
|
||||||
|
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
||||||
|
: 'dark:bg-[#1e1e1e] bg-white') +
|
||||||
|
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有页码
|
||||||
|
* @param {*} pagePrefix
|
||||||
|
* @param {*} page
|
||||||
|
* @param {*} currentPage
|
||||||
|
* @param {*} totalPage
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function generatePages(pagePrefix, page, currentPage, totalPage) {
|
||||||
|
const pages = []
|
||||||
|
const groupCount = 7 // 最多显示页签数
|
||||||
|
if (totalPage <= groupCount) {
|
||||||
|
for (let i = 1; i <= totalPage; i++) {
|
||||||
|
pages.push(getPageElement(i, page, pagePrefix))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push(getPageElement(1, page, pagePrefix))
|
||||||
|
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} className="-mt-2 mx-1">
|
||||||
|
...{' '}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||||
|
if (startPage + i < totalPage) {
|
||||||
|
pages.push(getPageElement(startPage + i, page, pagePrefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPage + dynamicGroupCount < totalPage) {
|
||||||
|
pages.push(<div key={-2}>... </div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push(getPageElement(totalPage, page, pagePrefix))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
export default PaginationNumber
|
||||||
87
themes/movie/components/SearchInput.js
Normal file
87
themes/movie/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, keyword, 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={keyword || ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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
|
||||||
68
themes/movie/components/SideBar.js
Normal file
68
themes/movie/components/SideBar.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
import Live2D from '@/components/Live2D'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import Announcement from './Announcement'
|
||||||
|
const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
|
||||||
|
|
||||||
|
export const SideBar = (props) => {
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
const { latestPosts, categoryOptions, notice } = 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">
|
||||||
|
{categoryOptions?.map(category => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={category.name}
|
||||||
|
href={`/category/${category.name}`}
|
||||||
|
passHref
|
||||||
|
legacyBehavior>
|
||||||
|
<li> <a href={`/category/${category.name}`} 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={`/${p.slug}`} className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<Announcement post={notice}/>
|
||||||
|
|
||||||
|
{siteConfig('COMMENT_WALINE_SERVER_URL') && siteConfig('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>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
themes/movie/components/TagItem.js
Normal file
24
themes/movie/components/TagItem.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function TagItem({ 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 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
themes/movie/components/TagItemMini.js
Normal file
21
themes/movie/components/TagItemMini.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const TagItemMini = ({ tag, selected = false }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tag}
|
||||||
|
href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||||
|
passHref
|
||||||
|
className={
|
||||||
|
'inline-block text-md font-extrabold rounded-xl text-shadow py-0.5 mr-2 text-[#2EBF8B]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="font-light">
|
||||||
|
{selected && <i className="mr-1 fa-tag" />}{' '}
|
||||||
|
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagItemMini
|
||||||
20
themes/movie/components/Title.js
Normal file
20
themes/movie/components/Title.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import NotionIcon from '@/components/NotionIcon'
|
||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题栏
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Title = (props) => {
|
||||||
|
const { post } = props
|
||||||
|
const title = post?.title || siteConfig('TITLE')
|
||||||
|
const description = post?.description || siteConfig('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"><NotionIcon icon={post?.pageIcon} />{title}</h1>
|
||||||
|
<p className="leading-loose text-gray-dark">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
14
themes/movie/config.js
Normal file
14
themes/movie/config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 主题配置文件
|
||||||
|
*/
|
||||||
|
const CONFIG = {
|
||||||
|
// 菜单配置
|
||||||
|
EXAMPLE_MENU_CATEGORY: true, // 显示分类
|
||||||
|
EXAMPLE_MENU_TAG: true, // 显示标签
|
||||||
|
EXAMPLE_MENU_ARCHIVE: true, // 显示归档
|
||||||
|
EXAMPLE_MENU_SEARCH: true, // 显示搜索
|
||||||
|
|
||||||
|
EXAMPLE_POST_LIST_COVER: true // 列表显示文章封面
|
||||||
|
|
||||||
|
}
|
||||||
|
export default CONFIG
|
||||||
303
themes/movie/index.js
Normal file
303
themes/movie/index.js
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import CONFIG from './config'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Header } from './components/Header'
|
||||||
|
import { Nav } from './components/Nav'
|
||||||
|
import { Footer } from './components/Footer'
|
||||||
|
import { Title } from './components/Title'
|
||||||
|
import { SideBar } from './components/SideBar'
|
||||||
|
import { BlogListPage } from './components/BlogListPage'
|
||||||
|
import { BlogListScroll } from './components/BlogListScroll'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import { ArticleLock } from './components/ArticleLock'
|
||||||
|
import { ArticleInfo } from './components/ArticleInfo'
|
||||||
|
import JumpToTopButton from './components/JumpToTopButton'
|
||||||
|
import NotionPage from '@/components/NotionPage'
|
||||||
|
import Comment from '@/components/Comment'
|
||||||
|
import ShareBar from '@/components/ShareBar'
|
||||||
|
import SearchInput from './components/SearchInput'
|
||||||
|
import replaceSearchResult from '@/components/Mark'
|
||||||
|
import { isBrowser } from '@/lib/utils'
|
||||||
|
import BlogListGroupByDate from './components/BlogListGroupByDate'
|
||||||
|
import CategoryItem from './components/CategoryItem'
|
||||||
|
import TagItem from './components/TagItem'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
import { Style } from './style'
|
||||||
|
import { siteConfig } from '@/lib/config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础布局框架
|
||||||
|
* 1.其它页面都嵌入在LayoutBase中
|
||||||
|
* 2.采用左右两侧布局,移动端使用顶部导航栏
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const LayoutBase = props => {
|
||||||
|
const { children } = props
|
||||||
|
const { onLoading, fullWidth } = useGlobal()
|
||||||
|
const router = useRouter()
|
||||||
|
const { category, tag } = props
|
||||||
|
// 顶部如果是按照分类或标签查看文章列表,列表顶部嵌入一个横幅
|
||||||
|
// 如果是搜索,则列表顶部嵌入 搜索框
|
||||||
|
let slotTop = null
|
||||||
|
if (category) {
|
||||||
|
slotTop = (
|
||||||
|
<div className="pb-12">
|
||||||
|
<i className="mr-1 fas fa-folder-open" />
|
||||||
|
{category}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (tag) {
|
||||||
|
slotTop = <div className="pb-12">#{tag}</div>
|
||||||
|
} else if (props.slotTop) {
|
||||||
|
slotTop = props.slotTop
|
||||||
|
} else if (router.route === '/search') {
|
||||||
|
// 嵌入一个搜索框在顶部
|
||||||
|
slotTop = (
|
||||||
|
<div className="pb-12">
|
||||||
|
<SearchInput {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加一个状态以触发 Transition 组件的动画
|
||||||
|
// const [showTransition, setShowTransition] = useState(true)
|
||||||
|
// useEffect(() => {
|
||||||
|
// // 当 location 或 children 发生变化时,触发动画
|
||||||
|
// setShowTransition(false)
|
||||||
|
// setTimeout(() => setShowTransition(true), 5)
|
||||||
|
// }, [onLoading])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="theme-example"
|
||||||
|
className={`${siteConfig('FONT_STYLE')} dark:text-gray-300 bg-white dark:bg-[#2A2A2A] scroll-smooth`}
|
||||||
|
>
|
||||||
|
<Style />
|
||||||
|
|
||||||
|
{/* 页头 */}
|
||||||
|
<Header {...props} />
|
||||||
|
|
||||||
|
{/* 主体 */}
|
||||||
|
<div id="container-inner" className="w-full relative z-10">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
{/* {fullWidth ? null : <Title {...props} />} */}
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="container-wrapper"
|
||||||
|
className={
|
||||||
|
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
|
||||||
|
? 'flex-row-reverse'
|
||||||
|
: '') +
|
||||||
|
'relative mx-auto justify-center md:flex items-start py-8 px-2'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className={`w-full ${fullWidth ? '' : ''} xl:px-32 lg:px-4`}>
|
||||||
|
<Transition
|
||||||
|
show={!onLoading}
|
||||||
|
appear={true}
|
||||||
|
enter="transition ease-in-out duration-700 transform order-first"
|
||||||
|
enterFrom="opacity-0 translate-y-16"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition ease-in-out duration-300 transform"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 -translate-y-16"
|
||||||
|
unmount={false}
|
||||||
|
>
|
||||||
|
{/* 嵌入模块 */}
|
||||||
|
{slotTop}
|
||||||
|
{children}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 页脚 */}
|
||||||
|
<Footer {...props} />
|
||||||
|
|
||||||
|
{/* 回顶按钮 */}
|
||||||
|
<div className="fixed right-4 bottom-4 z-10">
|
||||||
|
<JumpToTopButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页
|
||||||
|
* @param {*} props
|
||||||
|
* @returns 此主题首页就是列表
|
||||||
|
*/
|
||||||
|
const LayoutIndex = props => {
|
||||||
|
return <LayoutPostList {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章列表
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const LayoutPostList = props => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||||
|
<BlogListPage {...props} />
|
||||||
|
) : (
|
||||||
|
<BlogListScroll {...props} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章详情页
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const LayoutSlug = props => {
|
||||||
|
const { post, lock, validPassword } = props
|
||||||
|
const router = useRouter()
|
||||||
|
useEffect(() => {
|
||||||
|
// 404
|
||||||
|
if (!post) {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
if (isBrowser) {
|
||||||
|
const article = document.getElementById('notion-article')
|
||||||
|
if (!article) {
|
||||||
|
router.push('/404').then(() => {
|
||||||
|
console.warn('找不到页面', router.asPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
siteConfig('POST_WAITING_TIME_FOR_404') * 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [post])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{lock ? (
|
||||||
|
<ArticleLock validPassword={validPassword} />
|
||||||
|
) : (
|
||||||
|
<div id="article-wrapper" className="px-2">
|
||||||
|
<ArticleInfo post={post} />
|
||||||
|
<NotionPage post={post} />
|
||||||
|
<ShareBar post={post} />
|
||||||
|
<Comment frontMatter={post} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 404页
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const Layout404 = props => {
|
||||||
|
return <>404 Not found.</>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索页
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const LayoutSearch = props => {
|
||||||
|
const { keyword } = props
|
||||||
|
const router = useRouter()
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBrowser) {
|
||||||
|
// 高亮搜索到的结果
|
||||||
|
const container = document.getElementById('posts-wrapper')
|
||||||
|
if (keyword && container) {
|
||||||
|
replaceSearchResult({
|
||||||
|
doms: container,
|
||||||
|
search: keyword,
|
||||||
|
target: {
|
||||||
|
element: 'span',
|
||||||
|
className: 'text-red-500 border-b border-dashed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
return <LayoutPostList {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归档列表
|
||||||
|
* @param {*} props
|
||||||
|
* @returns 按照日期将文章分组排序
|
||||||
|
*/
|
||||||
|
const LayoutArchive = props => {
|
||||||
|
const { archivePosts } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-10 pb-20 md:py-12 p-3 min-h-screen w-full">
|
||||||
|
{Object.keys(archivePosts).map(archiveTitle => (
|
||||||
|
<BlogListGroupByDate
|
||||||
|
key={archiveTitle}
|
||||||
|
archiveTitle={archiveTitle}
|
||||||
|
archivePosts={archivePosts}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类列表
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const LayoutCategoryIndex = props => {
|
||||||
|
const { categoryOptions } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="category-list" className="duration-200 flex flex-wrap">
|
||||||
|
{categoryOptions?.map(category => (
|
||||||
|
<CategoryItem key={category.name} category={category} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签列表
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const LayoutTagIndex = props => {
|
||||||
|
const { tagOptions } = props
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="tags-list" className="duration-200 flex flex-wrap">
|
||||||
|
{tagOptions.map(tag => (
|
||||||
|
<TagItem key={tag.name} tag={tag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
CONFIG as THEME_CONFIG,
|
||||||
|
LayoutBase,
|
||||||
|
LayoutIndex,
|
||||||
|
LayoutSearch,
|
||||||
|
LayoutArchive,
|
||||||
|
LayoutSlug,
|
||||||
|
Layout404,
|
||||||
|
LayoutPostList,
|
||||||
|
LayoutCategoryIndex,
|
||||||
|
LayoutTagIndex
|
||||||
|
}
|
||||||
24
themes/movie/style.js
Normal file
24
themes/movie/style.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable react/no-unknown-property */
|
||||||
|
/**
|
||||||
|
* 此处样式只对当前主题生效
|
||||||
|
* 此处不支持tailwindCSS的 @apply 语法
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const Style = () => {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
// 底色
|
||||||
|
.dark body {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-movie {
|
||||||
|
box-shadow:
|
||||||
|
0 26px 58px 0 rgba(0, 0, 0, 0.22),
|
||||||
|
0 5px 14px 0 rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Style }
|
||||||
Reference in New Issue
Block a user