mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
init game theme
This commit is contained in:
107
themes/game/components/AdBlockerDetect.js
Normal file
107
themes/game/components/AdBlockerDetect.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 检测是否用了任意一种广告屏蔽插件
|
||||
* @returns {JSX.Element|null} 如果检测到广告屏蔽插件则返回提示信息,否则返回null
|
||||
*/
|
||||
export default function AdBlockerDetect() {
|
||||
const [isAdBlocker, setIsAdBlocker] = useState(false)
|
||||
const [noticeCountdown, setNoticeCountdown] = useState(10) // 广告拦截弹窗提示倒计时
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
let adsCheckCountdown = 10 // 广告拦截检测倒计时
|
||||
// GoogleAds 是否被拦截
|
||||
const adLoadTimer = setInterval(() => {
|
||||
if (window.adsbygoogle) {
|
||||
clearInterval(adLoadTimer)
|
||||
checkAdBlocker()
|
||||
} else {
|
||||
if (adsCheckCountdown > 1) {
|
||||
adsCheckCountdown--
|
||||
} else {
|
||||
clearInterval(adLoadTimer)
|
||||
setIsAdBlocker(true)
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(adLoadTimer)
|
||||
}, [router])
|
||||
|
||||
/**
|
||||
* 检测广告单元可见度
|
||||
*/
|
||||
const checkAdBlocker = () => {
|
||||
const ads = document.querySelectorAll('.adsbygoogle')
|
||||
if (ads.length === 0) {
|
||||
setIsAdBlocker(true)
|
||||
} else {
|
||||
let adEffect = false
|
||||
for (const ad of ads) {
|
||||
const adStyle = getComputedStyle(ad)
|
||||
if (adStyle.display !== 'none' && adStyle.visibility !== 'hidden') {
|
||||
adEffect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!adEffect) {
|
||||
setIsAdBlocker(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdBlocker) {
|
||||
const timer = setInterval(() => {
|
||||
setNoticeCountdown(prevCountdown => {
|
||||
if (prevCountdown <= 0) {
|
||||
clearInterval(timer)
|
||||
setIsAdBlocker(false)
|
||||
return 0
|
||||
} else {
|
||||
return prevCountdown - 1
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
return () => clearInterval(timer)
|
||||
}
|
||||
}, [isAdBlocker])
|
||||
|
||||
if (!isAdBlocker) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed w-screen h-screen z-40 flex justify-center items-center bg-black bg-opacity-75 top-0 left-0">
|
||||
<div className="fc-dialog-content z-50 bg-white rounded-md p-4 max-w-md">
|
||||
<div className="fc-dialog-headline">
|
||||
<h1 className="fc-dialog-headline-text text-3xl">
|
||||
Please allow ads on our site
|
||||
</h1>
|
||||
</div>
|
||||
<hr className="my-4" />
|
||||
<div className="fc-dialog-body">
|
||||
<p className="fc-dialog-body-text text-xl">
|
||||
{
|
||||
"Looks like you're using an ad blocker. We rely on advertising to help fund our site."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdBlocker(false)
|
||||
}}
|
||||
className="px-12 py-2 gap-2 bg-green-600 rounded text-white "
|
||||
>
|
||||
OK ({noticeCountdown})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
18
themes/game/components/Announcement.js
Normal file
18
themes/game/components/Announcement.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const NotionPage = dynamic(() => import('@/components/NotionPage'))
|
||||
|
||||
const Announcement = ({ notice, className }) => {
|
||||
if (notice?.blockMap) {
|
||||
return <div className={className}>
|
||||
<section id='announcement-wrapper' className='mb-10'>
|
||||
{notice && (<div id="announcement-content">
|
||||
<NotionPage post={notice} className='text-center ' />
|
||||
</div>)}
|
||||
</section>
|
||||
</div>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
export default Announcement
|
||||
33
themes/game/components/ArticleFooter.js
Normal file
33
themes/game/components/ArticleFooter.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleFooter = props => {
|
||||
const router = useRouter()
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <div className="flex justify-between font-medium text-gray-500 dark:text-gray-400">
|
||||
<a>
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
|
||||
>
|
||||
← {locale.POST.BACK}
|
||||
</button>
|
||||
</a>
|
||||
<a>
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
|
||||
>
|
||||
↑ {locale.POST.TOP}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
56
themes/game/components/ArticleInfo.js
Normal file
56
themes/game/components/ArticleInfo.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import TagItem from './TagItem'
|
||||
import md5 from 'js-md5'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import NotionIcon from '@/components/NotionIcon'
|
||||
|
||||
export const ArticleInfo = (props) => {
|
||||
const { post } = props
|
||||
|
||||
const emailHash = md5(siteConfig('CONTACT_EMAIL', '#'))
|
||||
|
||||
return <section className="flex-wrap flex mt-2 text-gray--600 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
|
||||
<h1 className="font-bold text-3xl text-black dark:text-white">
|
||||
<NotionIcon icon={post?.pageIcon} />{post?.title}
|
||||
</h1>
|
||||
|
||||
{post?.type !== 'Page' && <>
|
||||
<nav className="flex mt-7 items-start text-gray-500 dark:text-gray-400">
|
||||
<div className="flex mb-4">
|
||||
<a href={siteConfig('CONTACT_GITHUB', '#')} className="flex">
|
||||
<Image
|
||||
alt={siteConfig('AUTHOR')}
|
||||
width={24}
|
||||
height={24}
|
||||
src={`https://gravatar.com/avatar/${emailHash}`}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<p className="ml-2 md:block">{siteConfig('AUTHOR')}</p>
|
||||
</a>
|
||||
<span className="block"> / </span>
|
||||
</div>
|
||||
<div className="mr-2 mb-4 md:ml-0">
|
||||
{post?.publishDay}
|
||||
</div>
|
||||
{post?.tags && (
|
||||
<div className="flex flex-nowrap max-w-full overflow-x-auto article-tags">
|
||||
{post?.tags.map(tag => (
|
||||
<TagItem key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden busuanzi_container_page_pv mr-2">
|
||||
<i className='mr-1 fas fa-eye' />
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv" />
|
||||
</span>
|
||||
</nav>
|
||||
</>}
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
}
|
||||
53
themes/game/components/ArticleLock.js
Normal file
53
themes/game/components/ArticleLock.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleLock = props => {
|
||||
const { validPassword } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const submitPassword = () => {
|
||||
const p = document.getElementById('password')
|
||||
if (!validPassword(p?.value)) {
|
||||
const tips = document.getElementById('tips')
|
||||
if (tips) {
|
||||
tips.innerHTML = ''
|
||||
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const passwordInputRef = useRef(null)
|
||||
useEffect(() => {
|
||||
// 选中密码输入框并将其聚焦
|
||||
passwordInputRef.current.focus()
|
||||
}, [])
|
||||
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 '>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
<input id="password" type='password'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitPassword()
|
||||
}
|
||||
}}
|
||||
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
|
||||
className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'
|
||||
></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
44
themes/game/components/BlogArchiveItem.js
Normal file
44
themes/game/components/BlogArchiveItem.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 归档分组文章
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
|
||||
return (
|
||||
<div key={archiveTitle}>
|
||||
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
|
||||
{archiveTitle}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{archivePosts[archiveTitle].map(post => {
|
||||
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.date?.start_date}
|
||||
</span>{' '}
|
||||
|
||||
<Link
|
||||
href={url}
|
||||
passHref
|
||||
className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
|
||||
{post.title}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
themes/game/components/BlogListBar.js
Normal file
38
themes/game/components/BlogListBar.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useGameGlobal } from '..'
|
||||
import Tags from './Tags'
|
||||
|
||||
export default function BlogListBar(props) {
|
||||
const { tag, setFilterKey } = useGameGlobal()
|
||||
const handleSearchChange = val => {
|
||||
setFilterKey(val)
|
||||
}
|
||||
if (tag) {
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={tag ? `Search in #${tag}` : 'Search Articles'}
|
||||
className='outline-none block w-full border px-4 py-2 border-black bg-white text-black dark:bg-night dark:border-white dark:text-white'
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className='absolute right-3 top-3 h-5 w-5 text-black dark:text-white'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth='2'
|
||||
d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<Tags {...props} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
50
themes/game/components/BlogListPage.js
Normal file
50
themes/game/components/BlogListPage.js
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import BlogPost from './BlogPost'
|
||||
|
||||
export const BlogListPage = props => {
|
||||
const { page = 1, posts, postCount } = props
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE')))
|
||||
const currentPage = +page
|
||||
|
||||
const showPrev = currentPage > 1
|
||||
const showNext = currentPage < totalPage && posts?.length > 0
|
||||
const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
||||
|
||||
return (
|
||||
<div className="w-full md:pr-12 my-6">
|
||||
|
||||
<div id="posts-wrapper">
|
||||
{posts?.map(post => (
|
||||
<BlogPost key={post.id} post={post}/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs">
|
||||
<Link
|
||||
href={{ pathname: currentPage - 1 === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`, query: router.query.s ? { s: router.query.s } : {} }}
|
||||
className={`${showPrev ? ' ' : ' invisible block pointer-events-none '}no-underline py-2 px-3 rounded`}>
|
||||
|
||||
<button rel="prev" className="block cursor-pointer">
|
||||
← {locale.PAGINATION.PREV}
|
||||
</button>
|
||||
|
||||
</Link>
|
||||
<Link
|
||||
href={{ pathname: `${pagePrefix}/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}
|
||||
className={`${showNext ? ' ' : 'invisible pointer-events-none '} no-underline py-2 px-3 rounded`}>
|
||||
|
||||
<button rel="next" className="block cursor-pointer">
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</button>
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
83
themes/game/components/BlogListScroll.js
Normal file
83
themes/game/components/BlogListScroll.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { deepClone } from '@/lib/utils'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export const BlogListScroll = props => {
|
||||
const { posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
|
||||
let hasMore = false
|
||||
const postsToShow = posts && Array.isArray(posts)
|
||||
? deepClone(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))
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div id="posts-wrapper" className="w-full md:pr-12 mb-12" ref={targetRef}>
|
||||
{postsToShow.map(p => (
|
||||
<article key={p.id} className="mb-12" >
|
||||
<h2 className="mb-4">
|
||||
<Link
|
||||
href={`/${p.slug}`}
|
||||
className="text-black text-xl md:text-2xl no-underline hover:underline">
|
||||
{p.title}
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700">
|
||||
by <a href="#" className="text-gray-700">{siteConfig('AUTHOR')}</a> on {p.date?.start_date || p.createdTime}
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href="#" className="text-gray-700">{p.category}</a>
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-normal">
|
||||
{p.summary}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
themes/game/components/BlogPost.js
Normal file
41
themes/game/components/BlogPost.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Link from 'next/link'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
import NotionIcon from '@/components/NotionIcon'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
|
||||
const BlogPost = ({ post }) => {
|
||||
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
const showPreview = siteConfig('POST_LIST_PREVIEW') && post.blockMap
|
||||
|
||||
return (
|
||||
(<Link href={url}>
|
||||
|
||||
<article key={post.id} className="mb-6 md:mb-8">
|
||||
<header className="flex flex-col justify-between md:flex-row md:items-baseline">
|
||||
<h2 className="text-lg md:text-xl font-medium mb-2 cursor-pointer text-black dark:text-gray-100">
|
||||
<NotionIcon icon={post.pageIcon} />{post.title}
|
||||
</h2>
|
||||
<time className="flex-shrink-0 text-gray-600 dark:text-gray-400">
|
||||
{post?.publishDay}
|
||||
</time>
|
||||
</header>
|
||||
<main>
|
||||
{!showPreview && <p className="hidden md:block leading-8 text-gray-700 dark:text-gray-300">
|
||||
{post.summary}
|
||||
</p>}
|
||||
{showPreview && post?.blockMap && (
|
||||
<div className="overflow-ellipsis truncate">
|
||||
<NotionPage post={post} />
|
||||
<hr className='border-dashed py-4'/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</article>
|
||||
|
||||
</Link>)
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
153
themes/game/components/Draggable.js
Normal file
153
themes/game/components/Draggable.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 可拖拽组件
|
||||
*/
|
||||
export const Draggable = (props) => {
|
||||
const { children,stick } = props
|
||||
const draggableRef = useRef(null)
|
||||
const rafRef = useRef(null)
|
||||
const [moving, setMoving] = useState(false)
|
||||
let currentObj, offsetX, offsetY
|
||||
|
||||
useEffect(() => {
|
||||
const draggableElements = document.getElementsByClassName('draggable')
|
||||
|
||||
// 标准化鼠标事件对象
|
||||
function e(event) { // 定义事件对象标准化函数
|
||||
if (!event) { // 兼容IE浏览器
|
||||
event = window.event
|
||||
event.target = event.srcElement
|
||||
event.layerX = event.offsetX
|
||||
event.layerY = event.offsetY
|
||||
}
|
||||
// 移动端
|
||||
if (event.type === 'touchstart' || event.type === 'touchmove') {
|
||||
event.clientX = event.touches[0].clientX
|
||||
event.clientY = event.touches[0].clientY
|
||||
}
|
||||
|
||||
event.mx = event.pageX || event.clientX + document.body.scrollLeft
|
||||
// 计算鼠标指针的x轴距离
|
||||
event.my = event.pageY || event.clientY + document.body.scrollTop
|
||||
// 计算鼠标指针的y轴距离
|
||||
|
||||
return event // 返回标准化的事件对象
|
||||
}
|
||||
|
||||
// 定义鼠标事件处理函数
|
||||
// document.pointerdown = start
|
||||
document.onmousedown = start
|
||||
document.ontouchstart = start
|
||||
|
||||
function start (event) { // 按下鼠标时,初始化处理
|
||||
if (!draggableElements) return
|
||||
event = e(event)// 获取标准事件对象
|
||||
|
||||
for (const drag of draggableElements) {
|
||||
// 判断鼠标点击的区域是否是拖拽框内
|
||||
if (inDragBox(event, drag)) {
|
||||
currentObj = drag.firstElementChild
|
||||
}
|
||||
}
|
||||
if (currentObj) {
|
||||
if (event.type === 'touchstart') {
|
||||
event.preventDefault() // 阻止默认的滚动行为
|
||||
document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
|
||||
}
|
||||
|
||||
setMoving(true)
|
||||
offsetX = event.mx - currentObj.offsetLeft
|
||||
offsetY = event.my - currentObj.offsetTop
|
||||
|
||||
document.onmousemove = move// 注册鼠标移动事件处理函数
|
||||
document.ontouchmove = move
|
||||
document.onmouseup = stop// 注册松开鼠标事件处理函数
|
||||
document.ontouchend = stop
|
||||
}
|
||||
}
|
||||
|
||||
function move(event) { // 鼠标移动处理函数
|
||||
event = e(event)
|
||||
rafRef.current = requestAnimationFrame(() => updatePosition(event))
|
||||
}
|
||||
|
||||
const stop = (event) => {
|
||||
event = e(event)
|
||||
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
setMoving(false)
|
||||
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
|
||||
}
|
||||
|
||||
const updatePosition = (event) => {
|
||||
if (currentObj) {
|
||||
const left = event.mx - offsetX
|
||||
const top = event.my - offsetY
|
||||
currentObj.style.left = left + 'px'
|
||||
currentObj.style.top = top + 'px'
|
||||
checkInWindow()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标是否在可拖拽区域内
|
||||
* @param {*} event
|
||||
* @returns
|
||||
*/
|
||||
function inDragBox(event, drag) {
|
||||
const { clientX, clientY } = event // 鼠标位置
|
||||
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild // 窗口位置
|
||||
const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth
|
||||
const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight
|
||||
|
||||
if (horizontal && vertical) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 若超出窗口则吸附。
|
||||
*/
|
||||
function checkInWindow() {
|
||||
// 检查是否悬浮在窗口内
|
||||
for (const drag of draggableElements) {
|
||||
// 判断鼠标点击的区域是否是拖拽框内
|
||||
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild
|
||||
const { clientHeight, clientWidth } = document.documentElement
|
||||
if (offsetTop < 0) {
|
||||
drag.firstElementChild.style.top = 0
|
||||
}
|
||||
if (offsetTop > (clientHeight - offsetHeight)) {
|
||||
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
|
||||
}
|
||||
if (offsetLeft < 0) {
|
||||
drag.firstElementChild.style.left = 0
|
||||
}
|
||||
if (offsetLeft > (clientWidth - offsetWidth)) {
|
||||
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
|
||||
}
|
||||
if(stick==='left'){
|
||||
drag.firstElementChild.style.left = 0 + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', checkInWindow)
|
||||
|
||||
return () => {
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkInWindow)
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
Draggable.defaultProps = { left: 0, top: 0 }
|
||||
35
themes/game/components/ExampleRecentComments.js
Normal file
35
themes/game/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
|
||||
29
themes/game/components/Footer.js
Normal file
29
themes/game/components/Footer.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import Vercel from '@/components/Vercel'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
export const Footer = (props) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const { post } = props
|
||||
const fullWidth = post?.fullWidth ?? false
|
||||
const since = siteConfig('SINCE')
|
||||
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
|
||||
|
||||
return <footer
|
||||
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 transition-all ${
|
||||
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
>
|
||||
<DarkModeButton className='text-center py-4'/>
|
||||
<hr className="border-gray-200 dark:border-gray-600" />
|
||||
<div className="my-4 text-sm leading-6">
|
||||
<div className="flex align-baseline justify-between flex-wrap">
|
||||
<p>
|
||||
© {siteConfig('AUTHOR')} {copyrightDate}
|
||||
</p>
|
||||
<Vercel />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
39
themes/game/components/FullScreen.js
Normal file
39
themes/game/components/FullScreen.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
/**
|
||||
* 全屏按钮
|
||||
* @returns
|
||||
*/
|
||||
export default function FullScreen() {
|
||||
function toggleFullScreen() {
|
||||
// window.scrollTo(0, 2)
|
||||
document?.querySelector('#game-wrapper')?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
})
|
||||
// console.log(document?.getElementById('game-wrapper')?.contentWindow)
|
||||
document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group text-white w-full justify-center items-center flex rounded-lg m-2 md:m-0 p-2 hover:bg-gray-700 bg-[#1F2030] md:rounded-none md:bg-none"
|
||||
onClick={toggleFullScreen}
|
||||
>
|
||||
<Image
|
||||
width={18}
|
||||
height={18}
|
||||
src="/svg/fullscreen-alt.svg"
|
||||
alt="full screen"
|
||||
title="full screen"
|
||||
className="cursor-pointer group-hover:scale-125 transition-all duration-150 "
|
||||
/>
|
||||
<span className="h-full flex mx-2 md:hidden items-center select-none">
|
||||
FullScreen
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
164
themes/game/components/GameListIndexCombine.js
Normal file
164
themes/game/components/GameListIndexCombine.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { AdSlot } from '@/components/GoogleAdsense'
|
||||
import { deepClone } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 游戏列表
|
||||
* @returns
|
||||
*/
|
||||
export const GameListIndexCombine = ({ games }) => {
|
||||
const gamesClone = deepClone(games)
|
||||
|
||||
gamesClone?.sort((a, b) => {
|
||||
const orderA = a.order || 999
|
||||
const orderB = b.order || 999
|
||||
|
||||
return orderA - orderB
|
||||
})
|
||||
|
||||
// 构造一个List<Component>
|
||||
const components = []
|
||||
|
||||
// 根据序号随机大小;或根据game.recommend 决定
|
||||
const recommend = true
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
if (recommend) {
|
||||
// 4合一卡组
|
||||
let groupItems = []
|
||||
while (gamesClone?.length > 0) {
|
||||
index++
|
||||
|
||||
// 广告位
|
||||
if (index % 9 === 0) {
|
||||
components.push(<GameAd key={index} />)
|
||||
continue
|
||||
}
|
||||
|
||||
// 试图将4合一卡组塞满
|
||||
while (gamesClone?.length > 0 && groupItems.length < 4) {
|
||||
const item = gamesClone.shift()
|
||||
if (item.recommend) {
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
break
|
||||
} else {
|
||||
groupItems.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (groupItems.length === 4 || (gamesClone.length === 0 && groupItems.length > 4)) {
|
||||
components.push(<GameItemGroup key={index} items={groupItems} />)
|
||||
groupItems = []
|
||||
} else {
|
||||
while (groupItems.length > 0) {
|
||||
const item = groupItems.shift()
|
||||
components.push(<GameItem key={index++} item={item} isLargeCard={true} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (gamesClone?.length > 0) {
|
||||
index++
|
||||
|
||||
if (index % 6 === 0) {
|
||||
components.push(<GameAd key={index} />)
|
||||
} else if (index % 2 === 0 && gamesClone?.length >= 4) {
|
||||
// 如果是偶数,则从游戏列表中退出4个组成大卡牌
|
||||
const groupItems = []
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
groupItems.push(gamesClone.shift())
|
||||
}
|
||||
components.push(<GameItemGroup key={index} items={groupItems} />)
|
||||
} else {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper flex justify-center w-full px-2'>
|
||||
<div className='game-grid mx-auto w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个广告游戏大卡
|
||||
* @returns
|
||||
*/
|
||||
const GameAd = () => {
|
||||
return (
|
||||
<div className='card-group border border-gray-600 rounded game-ad h-[20rem] w-full overflow-hidden'>
|
||||
<AdSlot type='flow' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 4卡组成一个大卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItemGroup = ({ items }) => {
|
||||
return (
|
||||
<div className='card-group h-[20rem] w-full grid grid-cols-2 grid-rows-2 gap-2'>
|
||||
{items.map((item, index) => (
|
||||
<GameItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItem = ({ item, isLargeCard }) => {
|
||||
const { id, title, img, video } = item
|
||||
const [showType, setShowType] = useState('img') // img or video
|
||||
return (
|
||||
<a
|
||||
href={`/game/${id}`}
|
||||
onMouseOver={() => {
|
||||
setShowType('video')
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setShowType('img')
|
||||
}}
|
||||
title={title}
|
||||
className={`card-single ${
|
||||
isLargeCard ? 'h-[20rem]' : 'h-full'
|
||||
} w-full relative shadow rounded-md overflow-hidden flex justify-center items-center
|
||||
group hover:border-purple-400`}>
|
||||
<div className='text-center absolute bottom-0 invisible group-hover:bottom-2 group-hover:visible transition-all duration-200 text-white z-30'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-0 group-hover:opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video
|
||||
className={`z-10 object-cover w-full ${isLargeCard ? 'h-[20rem]' : 'h-full'} absolute overflow-hidden`}
|
||||
loop='true'
|
||||
autoPlay
|
||||
preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
69
themes/game/components/GameListNormal.js
Normal file
69
themes/game/components/GameListNormal.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { deepClone } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 游戏列表- 关联游戏,在详情页展示
|
||||
* @returns
|
||||
*/
|
||||
export const GameListNormal = ({ games, maxCount = 18 }) => {
|
||||
const gamesClone = deepClone(games)
|
||||
|
||||
// 构造一个List<Component>
|
||||
const components = []
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper w-full'>
|
||||
<div className='game-grid mx-auto w-full h-full grid grid-cols-3 gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItem = ({ item }) => {
|
||||
const { id, title, img, video } = item
|
||||
const [showType, setShowType] = useState('img') // img or video
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/game/${id}`}
|
||||
onMouseOver={() => {
|
||||
setShowType('video')
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setShowType('img')
|
||||
}}
|
||||
title={title}
|
||||
className={`card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center
|
||||
group hover:border-purple-400`}>
|
||||
<div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>{title}</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className='z-10 object-cover w-auto h-28 absolute overflow-hidden' loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img className='w-full h-full absolute object-cover' src={img} alt={title} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
76
themes/game/components/GameListRealate.js
Normal file
76
themes/game/components/GameListRealate.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { deepClone } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 游戏列表- 关联游戏,在详情页展示
|
||||
* @returns
|
||||
*/
|
||||
export const GameListRelate = ({ games }) => {
|
||||
const gamesClone = deepClone(games)
|
||||
|
||||
// 构造一个List<Component>
|
||||
const components = []
|
||||
const maxCount = 24
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper w-full max-w-full overflow-x-auto'>
|
||||
<div className='game-grid grid grid-flow-col gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItem = ({ item, isLargeCard }) => {
|
||||
const { id, title, img, video } = item
|
||||
const [showType, setShowType] = useState('img') // img or video
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/game/${id}`}
|
||||
onMouseOver={() => {
|
||||
setShowType('video')
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setShowType('img')
|
||||
}}
|
||||
title={title}
|
||||
className={`card-single w-24 h-24 relative shadow rounded-md overflow-hidden flex justify-center items-center
|
||||
group hover:border-purple-400`}>
|
||||
<div className='text-sm text-center absolute bottom-0 invisible group-hover:bottom-2 group-hover:visible transition-all duration-200 text-white z-30'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-0 group-hover:opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className={`z-10 object-cover w-full h-24 absolute overflow-hidden`} loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-24 h-24 absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
83
themes/game/components/GameListRecent.js
Normal file
83
themes/game/components/GameListRecent.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { deepClone } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 游戏列表- 最近游戏
|
||||
* @returns
|
||||
*/
|
||||
export const GameListRecent = ({ maxCount = 14 }) => {
|
||||
const { recentGames } = useGlobal()
|
||||
const gamesClone = deepClone(recentGames)
|
||||
// 构造一个List<Component>
|
||||
const components = []
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone?.shift()
|
||||
if (item) {
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (components.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='text-white text-lg pb-1 p-2'>Recent Played</div>
|
||||
<div className='game-list-recent-wrapper w-full max-w-full overflow-x-auto p-2'>
|
||||
<div className='game-grid md:flex grid grid-flow-col gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItem = ({ item }) => {
|
||||
const { id, title, img, video } = item || {}
|
||||
const [showType, setShowType] = useState('img') // img or video
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/game/${id}`}
|
||||
onMouseOver={() => {
|
||||
setShowType('video')
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setShowType('img')
|
||||
}}
|
||||
title={title}
|
||||
className={`card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center
|
||||
group hover:border-purple-400`}>
|
||||
<div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>{title}</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className='z-10 object-cover w-auto h-28 absolute overflow-hidden' loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
32
themes/game/components/Header.js
Normal file
32
themes/game/components/Header.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Image from 'next/image'
|
||||
import Logo from './Logo'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 顶栏
|
||||
* @returns
|
||||
*/
|
||||
export default function Header() {
|
||||
const { setSideBarVisible } = useGlobal()
|
||||
return (
|
||||
<header className="z-20">
|
||||
<div className="w-full h-16 rounded-md bg-[#1F2030] flex justify-between items-center px-4">
|
||||
<Logo />
|
||||
|
||||
<button
|
||||
className="flex xl:hidden"
|
||||
onClick={() => {
|
||||
setSideBarVisible(true)
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/svg/search.svg"
|
||||
className="mr-2"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
18
themes/game/components/JumpToTopButton.js
Normal file
18
themes/game/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
|
||||
14
themes/game/components/Logo.js
Normal file
14
themes/game/components/Logo.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
|
||||
/* eslint-disable @next/next/no-html-link-for-pages */
|
||||
export default function Logo() {
|
||||
return (
|
||||
<Link passHref href='/' className='logo rounded cursor-pointer flex flex-col items-center'>
|
||||
<div className='w-full'>
|
||||
<h1 className='text-2xl text-white font-bold font-serif'>{siteConfig('TITLE')}</h1>
|
||||
<h2 className='text-xs text-gray-400 whitespace-nowrap'>{siteConfig('BIO')}</h2>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
11
themes/game/components/LogoMini.js
Normal file
11
themes/game/components/LogoMini.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
|
||||
/* eslint-disable @next/next/no-html-link-for-pages */
|
||||
export default function LogoMini() {
|
||||
return (
|
||||
<Link href='/' className='logo rounded cursor-pointer flex items-center text-xl text-white font-bold font-serif'>
|
||||
{siteConfig('TITLE')?.charAt(0)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
55
themes/game/components/MenuItemCollapse.js
Normal file
55
themes/game/components/MenuItemCollapse.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import Collapse from '@/components/Collapse'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 折叠菜单
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const MenuItemCollapse = (props) => {
|
||||
const { link } = props
|
||||
const [show, changeShow] = useState(false)
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
const [isOpen, changeIsOpen] = useState(false)
|
||||
|
||||
const toggleShow = () => {
|
||||
changeShow(!show)
|
||||
}
|
||||
|
||||
const toggleOpenSubMenu = () => {
|
||||
changeIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} >
|
||||
{!hasSubMenu && <Link
|
||||
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
</Link>}
|
||||
{hasSubMenu && <div
|
||||
onClick={hasSubMenu ? toggleOpenSubMenu : null}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
<i className='px-2 fa fa-plus text-gray-400'></i>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* 折叠子菜单 */}
|
||||
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return <div key={index} className='font-extralight dark:bg-black text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 border-b dark:border-gray-800 py-3 pr-6'>
|
||||
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
|
||||
<span className='text-xs'>{sLink.title}</span>
|
||||
</Link>
|
||||
</div>
|
||||
})}
|
||||
</Collapse>}
|
||||
</>
|
||||
}
|
||||
45
themes/game/components/MenuItemDrop.js
Normal file
45
themes/game/components/MenuItemDrop.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MenuItemDrop = ({ link }) => {
|
||||
const [show, changeShow] = useState(false)
|
||||
// const show = true
|
||||
// const changeShow = () => {}
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
return <li className='mx-3 my-2' >
|
||||
<div className='cursor-pointer ' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}>
|
||||
{!hasSubMenu &&
|
||||
<div className="block text-black dark:text-gray-50 nav" >
|
||||
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} >
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
{hasSubMenu &&
|
||||
<div className='block text-black dark:text-gray-50 nav'>
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* 子菜单 */}
|
||||
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return <div key={index} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
|
||||
<Link href={sLink.to} 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>
|
||||
</div>
|
||||
})}
|
||||
</ul>}
|
||||
|
||||
</div>
|
||||
|
||||
</li>
|
||||
}
|
||||
30
themes/game/components/MenuList.js
Normal file
30
themes/game/components/MenuList.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link'
|
||||
import { useGameGlobal } from '..'
|
||||
|
||||
export const MenuList = () => {
|
||||
const { setSideBarVisible } = useGameGlobal()
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
<li className='text-white py-4 px-2 font-bold hover:underline'>
|
||||
<Link href='/' passHref>
|
||||
<span className='flex items-center gap-2'>
|
||||
<i className='fas fa-home' />
|
||||
Home
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className='text-white py-4 px-2 font-bold hover:underline'>
|
||||
<button
|
||||
className='flex items-center gap-2'
|
||||
onClick={() => {
|
||||
setSideBarVisible(true)
|
||||
}}>
|
||||
<i className='fas fa-search' />
|
||||
<span>Search</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
152
themes/game/components/Nav.js
Normal file
152
themes/game/components/Nav.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import Collapse from '@/components/Collapse'
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import CONFIG from '../config'
|
||||
import { MenuItemCollapse } from './MenuItemCollapse'
|
||||
import { MenuItemDrop } from './MenuItemDrop'
|
||||
import RandomPostButton from './RandomPostButton'
|
||||
import SearchButton from './SearchButton'
|
||||
import { SvgIcon } from './SvgIcon'
|
||||
|
||||
const Nav = props => {
|
||||
const { navBarTitle, fullWidth, siteInfo } = props
|
||||
const useSticky = !JSON.parse(siteConfig('GAME_AUTO_COLLAPSE_NAV_BAR', true))
|
||||
const navRef = useRef(null)
|
||||
const sentinalRef = useRef([])
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current && useSticky) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current?.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current?.classList.remove('sticky-nav-full')
|
||||
}
|
||||
} else {
|
||||
navRef.current?.classList.add('remove-sticky')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const obvserver = new window.IntersectionObserver(handler)
|
||||
obvserver.observe(sentinalRef.current)
|
||||
return () => {
|
||||
if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
}
|
||||
}, [sentinalRef])
|
||||
return (
|
||||
<>
|
||||
<div className='observer-element h-4 md:h-12' ref={sentinalRef}></div>
|
||||
<div
|
||||
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 md:mb-12 py-8 bg-opacity-60 ${
|
||||
!fullWidth ? 'max-w-3xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
id='sticky-nav'
|
||||
ref={navRef}>
|
||||
<div className='flex items-center'>
|
||||
<Link href='/' aria-label={siteConfig('TITLE')}>
|
||||
<div className='h-6 w-6'>
|
||||
{/* <SvgIcon/> */}
|
||||
{siteConfig('GAME_NAV_NOTION_ICON', null, CONFIG) ? (
|
||||
<LazyImage src={siteInfo?.icon} width={24} height={24} alt={siteConfig('AUTHOR')} />
|
||||
) : (
|
||||
<SvgIcon />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{navBarTitle ? (
|
||||
<p className='ml-2 font-medium text-gray-800 dark:text-gray-300 header-name'>{navBarTitle}</p>
|
||||
) : (
|
||||
<p className='ml-2 font-medium text-gray-800 dark:text-gray-300 header-name whitespace-nowrap'>
|
||||
{siteConfig('TITLE')}
|
||||
{/* ,{' '}<span className="font-normal">{siteConfig('DESCRIPTION')}</span> */}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar {...props} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const NavBar = props => {
|
||||
const { customMenu, customNav } = props
|
||||
const [isOpen, changeOpen] = useState(false)
|
||||
const toggleOpen = () => {
|
||||
changeOpen(!isOpen)
|
||||
}
|
||||
const collapseRef = useRef(null)
|
||||
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{
|
||||
id: 2,
|
||||
name: locale.NAV.RSS,
|
||||
to: '/feed',
|
||||
show: siteConfig('ENABLE_RSS') && siteConfig('GAME_MENU_RSS', null, CONFIG),
|
||||
target: '_blank'
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-search',
|
||||
name: locale.NAV.SEARCH,
|
||||
to: '/search',
|
||||
show: siteConfig('GAME_MENU_SEARCH', null, CONFIG)
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-archive',
|
||||
name: locale.NAV.ARCHIVE,
|
||||
to: '/archive',
|
||||
show: siteConfig('GAME_MENU_ARCHIVE', null, CONFIG)
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-folder',
|
||||
name: locale.COMMON.CATEGORY,
|
||||
to: '/category',
|
||||
show: siteConfig('GAME_MENU_CATEGORY', null, CONFIG)
|
||||
},
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('GAME_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 (
|
||||
<div className='flex-shrink-0 flex'>
|
||||
<ul className='hidden md:flex flex-row'>
|
||||
{links?.map((link, index) => (
|
||||
<MenuItemDrop key={index} link={link} />
|
||||
))}
|
||||
</ul>
|
||||
<div className='md:hidden'>
|
||||
<Collapse collapseRef={collapseRef} isOpen={isOpen} type='vertical' className='fixed top-16 right-6'>
|
||||
<div className='dark:border-black bg-white dark:bg-black rounded border p-2 text-sm'>
|
||||
{links?.map((link, index) => (
|
||||
<MenuItemCollapse
|
||||
key={index}
|
||||
link={link}
|
||||
onHeightChange={param => collapseRef.current?.updateCollapseHeight(param)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
{JSON.parse(siteConfig('GAME_MENU_RANDOM_POST', null, CONFIG)) && <RandomPostButton {...props} />}
|
||||
{JSON.parse(siteConfig('GAME_MENU_SEARCH_BUTTON', null, CONFIG)) && <SearchButton {...props} />}
|
||||
<i
|
||||
onClick={toggleOpen}
|
||||
className='fas fa-bars cursor-pointer px-5 flex justify-center items-center md:hidden'></i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
9
themes/game/components/NavBar.js
Normal file
9
themes/game/components/NavBar.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MenuList } from './MenuList'
|
||||
|
||||
export default function NavBar({ className }) {
|
||||
return (
|
||||
<nav className={className}>
|
||||
<MenuList />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
26
themes/game/components/RandomPostButton.js
Normal file
26
themes/game/components/RandomPostButton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 随机跳转到一个文章
|
||||
*/
|
||||
export default function RandomPostButton(props) {
|
||||
const { latestPosts } = props
|
||||
const router = useRouter()
|
||||
const { locale } = useGlobal()
|
||||
/**
|
||||
* 随机跳转文章
|
||||
*/
|
||||
function handleClick() {
|
||||
const randomIndex = Math.floor(Math.random() * latestPosts.length)
|
||||
const randomPost = latestPosts[randomIndex]
|
||||
router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div title={locale.MENU.WALK_AROUND} className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all' onClick={handleClick}>
|
||||
<i className="fa-solid fa-podcast"></i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
themes/game/components/SearchButton.js
Normal file
34
themes/game/components/SearchButton.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGameGlobal } from '..'
|
||||
|
||||
/**
|
||||
* 搜索按钮
|
||||
* @returns
|
||||
*/
|
||||
export default function SearchButton(props) {
|
||||
const { locale } = useGlobal()
|
||||
const { searchModal } = useGameGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
function handleSearch() {
|
||||
if (siteConfig('ALGOLIA_APP_ID')) {
|
||||
searchModal.current.openSearch()
|
||||
} else {
|
||||
router.push('/search')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={handleSearch}
|
||||
title={locale.NAV.SEARCH}
|
||||
alt={locale.NAV.SEARCH}
|
||||
className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all'>
|
||||
<i title={locale.NAV.SEARCH} className='fa-solid fa-magnifying-glass' />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
88
themes/game/components/SearchInput.js
Normal file
88
themes/game/components/SearchInput.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
|
||||
let lock = false
|
||||
|
||||
const SearchInput = props => {
|
||||
const { tag, keyword, cRef } = props
|
||||
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={tag ? `${locale.SEARCH.TAGS} #${tag}` : `${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
|
||||
17
themes/game/components/SearchNavBar.js
Normal file
17
themes/game/components/SearchNavBar.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import SearchInput from './SearchInput'
|
||||
import Tags from './Tags'
|
||||
|
||||
/**
|
||||
* 搜索页面上方嵌入内容
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function SearchNavBar(props) {
|
||||
return (<>
|
||||
<div className='pb-12'>
|
||||
<SearchInput {...props} />
|
||||
</div>
|
||||
|
||||
<Tags {...props}/>
|
||||
</>)
|
||||
}
|
||||
65
themes/game/components/SideBar.js
Normal file
65
themes/game/components/SideBar.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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'
|
||||
const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
|
||||
|
||||
export const SideBar = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { latestPosts, categories } = props
|
||||
return (
|
||||
<div className="w-full md:w-64 sticky top-8">
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.CATEGORY}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{categories?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.LATEST_POSTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{latestPosts?.map(p => {
|
||||
return (
|
||||
<Link key={p.id} href={`/${p.slug}`} passHref legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{siteConfig('COMMENT_WALINE_SERVER_URL') && JSON.parse(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>
|
||||
);
|
||||
}
|
||||
60
themes/game/components/SideBarContent.js
Normal file
60
themes/game/components/SideBarContent.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useGameGlobal } from '..'
|
||||
import { GameListNormal } from './GameListNormal'
|
||||
import Logo from './Logo'
|
||||
|
||||
/**
|
||||
* 侧拉抽屉的内容
|
||||
*/
|
||||
export default function SideBarContent() {
|
||||
const { allGames, sideBarVisible, setSideBarVisible, filterGames, setFilterGames } = useGameGlobal()
|
||||
const inputRef = useRef(null) // 创建对输入框的引用
|
||||
|
||||
useEffect(() => {
|
||||
if (sideBarVisible) {
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus() // 在组件渲染后聚焦输入框
|
||||
}, 100)
|
||||
}
|
||||
}, [sideBarVisible, inputRef])
|
||||
|
||||
const handleSearch = e => {
|
||||
const search = e.target.value
|
||||
if (!search || search === '') {
|
||||
setFilterGames(allGames?.filter(item => item.recommend))
|
||||
return
|
||||
}
|
||||
setFilterGames(
|
||||
allGames?.filter(item => {
|
||||
return (
|
||||
item.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.id.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.id.toLowerCase().replace('-', '').includes(search.toLowerCase().replace('-', ''))
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='px-3'>
|
||||
<div className='py-2 flex justify-between'>
|
||||
<Logo />
|
||||
<button
|
||||
onClick={() => {
|
||||
setSideBarVisible(false)
|
||||
}}>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
autoFocus
|
||||
id='search-input'
|
||||
ref={inputRef} // 将引用绑定到输入框
|
||||
className='w-full h-12 rounded px-3 text-black'
|
||||
onChange={handleSearch}
|
||||
placeholder='Input the name of game'></input>
|
||||
<div className='py-4'>
|
||||
<GameListNormal games={filterGames} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
59
themes/game/components/SideBarDrawer.js
Normal file
59
themes/game/components/SideBarDrawer.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 侧边栏抽屉面板,可以从侧面拉出
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
const sideBarDrawerRouteListener = () => {
|
||||
switchSideDrawerVisible(false)
|
||||
}
|
||||
router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
// 点击按钮更改侧边抽屉状态
|
||||
const switchSideDrawerVisible = showStatus => {
|
||||
if (showStatus) {
|
||||
onOpen && onOpen()
|
||||
} else {
|
||||
onClose && onClose()
|
||||
}
|
||||
const sideBarDrawer = window.document.getElementById('sidebar-drawer')
|
||||
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
|
||||
|
||||
if (showStatus) {
|
||||
sideBarDrawer?.classList.replace('-ml-96', 'ml-0')
|
||||
sideBarDrawerBackground?.classList.replace('hidden', 'block')
|
||||
} else {
|
||||
sideBarDrawer?.classList.replace('ml-0', '-ml-96')
|
||||
sideBarDrawerBackground?.classList.replace('block', 'hidden')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='sidebar-wrapper' className={`top-0 ${className}`}>
|
||||
<div
|
||||
id='sidebar-drawer'
|
||||
className={`${isOpen ? 'ml-0 visible opacity-100' : '-ml-96 invisible opacity-0'} w-96 bg-black shadow-black shadow-lg flex flex-col duration-300 fixed h-full left-0 overflow-y-scroll scroll-hidden top-0 z-30`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 背景蒙版 */}
|
||||
<div
|
||||
id='sidebar-drawer-background'
|
||||
onClick={() => {
|
||||
switchSideDrawerVisible(false)
|
||||
}}
|
||||
className={`${isOpen ? 'visible opacity-100' : 'invisible opacity-0 '} animate__animated animate__fadeIn fixed top-0 duration-300 left-0 z-20 w-full h-full bg-black/70`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SideBarDrawer
|
||||
29
themes/game/components/SvgIcon.js
Normal file
29
themes/game/components/SvgIcon.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export const SvgIcon = () => {
|
||||
return <svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
className="fill-current text-black dark:text-white"
|
||||
/>
|
||||
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45) scale(39.598)"
|
||||
>
|
||||
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
||||
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
11
themes/game/components/TagItem.js
Normal file
11
themes/game/components/TagItem.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItem = ({ tag }) => (
|
||||
(<Link href={`/tag/${encodeURIComponent(tag)}`}>
|
||||
<p className="mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600">
|
||||
{tag}
|
||||
</p>
|
||||
</Link>)
|
||||
)
|
||||
|
||||
export default TagItem
|
||||
38
themes/game/components/Tags.js
Normal file
38
themes/game/components/Tags.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const Tags = props => {
|
||||
const { tagOptions, tag } = props
|
||||
const currentTag = tag
|
||||
if (!tagOptions) return null
|
||||
return (
|
||||
<div className="tag-container">
|
||||
<ul className="flex max-w-full mt-4 overflow-x-auto">
|
||||
{Object.keys(tagOptions).map(key => {
|
||||
const tag = tagOptions[key]
|
||||
const selected = tag.name === currentTag
|
||||
return (
|
||||
<li
|
||||
key={tag.id}
|
||||
className={`mr-3 font-medium border whitespace-nowrap dark:text-gray-300 ${
|
||||
selected
|
||||
? 'text-white bg-black border-black dark:bg-gray-600 dark:border-gray-600'
|
||||
: 'bg-gray-100 border-gray-100 text-gray-400 dark:bg-night dark:border-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={selected ? '/search' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||
className="px-4 py-2 block">
|
||||
|
||||
{`${tag.name} (${tag.count})`}
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tags
|
||||
19
themes/game/components/Title.js
Normal file
19
themes/game/components/Title.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 标题栏
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Title = (props) => {
|
||||
const { post } = props
|
||||
const title = post?.title || siteConfig('DESCRIPTION')
|
||||
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">{title}</h1>
|
||||
<p className="leading-loose text-gray-dark">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
15
themes/game/config.js
Normal file
15
themes/game/config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const CONFIG = {
|
||||
GAME_NAV_NOTION_ICON: true, // 是否读取Notion图标作为站点头像 ; 否则默认显示黑色SVG方块
|
||||
|
||||
// 特殊菜单
|
||||
GAME_MENU_RANDOM_POST: true, // 是否显示随机跳转文章按钮
|
||||
GAME_MENU_SEARCH_BUTTON: true, // 是否显示搜索按钮,该按钮支持Algolia搜索
|
||||
|
||||
// 默认菜单配置 (开启自定义菜单后,以下配置则失效,请在Notion中自行配置菜单)
|
||||
GAME_MENU_CATEGORY: false, // 显示分类
|
||||
GAME_MENU_TAG: true, // 显示标签
|
||||
GAME_MENU_ARCHIVE: false, // 显示归档
|
||||
GAME_MENU_SEARCH: true, // 显示搜索
|
||||
GAME_MENU_RSS: false // 显示订阅
|
||||
}
|
||||
export default CONFIG
|
||||
350
themes/game/index.js
Normal file
350
themes/game/index.js
Normal file
@@ -0,0 +1,350 @@
|
||||
import Comment from '@/components/Comment'
|
||||
import { AdSlot } from '@/components/GoogleAdsense'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { deepClone, isBrowser } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import Announcement from './components/Announcement'
|
||||
import { ArticleFooter } from './components/ArticleFooter'
|
||||
import { ArticleInfo } from './components/ArticleInfo'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import BlogArchiveItem from './components/BlogArchiveItem'
|
||||
import { BlogListPage } from './components/BlogListPage'
|
||||
import { BlogListScroll } from './components/BlogListScroll'
|
||||
import { Footer } from './components/Footer'
|
||||
import Header from './components/Header'
|
||||
import NavBar from './components/NavBar'
|
||||
import SearchNavBar from './components/SearchNavBar'
|
||||
import SideBarContent from './components/SideBarContent'
|
||||
import SideBarDrawer from './components/SideBarDrawer'
|
||||
import CONFIG from './config'
|
||||
import { Style } from './style'
|
||||
|
||||
// const AlgoliaSearchModal = dynamic(() => import('@/components/AlgoliaSearchModal'), { ssr: false })
|
||||
|
||||
// 主题全局状态
|
||||
const ThemeGlobalGame = createContext()
|
||||
export const useGameGlobal = () => useContext(ThemeGlobalGame)
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { allNavPages, children } = props
|
||||
|
||||
// const fullWidth = post?.fullWidth ?? false
|
||||
// const { onLoading } = useGlobal()
|
||||
const searchModal = useRef(null)
|
||||
// 在列表中进行实时过滤
|
||||
const [filterKey, setFilterKey] = useState('')
|
||||
|
||||
const [filterGames, setFilterGames] = useState(deepClone(allNavPages?.filter(item => item.recommend)))
|
||||
const [recentGames, setRecentGames] = useState([])
|
||||
const [sideBarVisible, setSideBarVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setRecentGames(localStorage.getItem('recent_games') ? JSON.parse(localStorage.getItem('recent_games')) : [])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeGlobalGame.Provider
|
||||
value={{
|
||||
searchModal,
|
||||
filterKey,
|
||||
setFilterKey,
|
||||
recentGames,
|
||||
setRecentGames,
|
||||
filterGames,
|
||||
setFilterGames,
|
||||
sideBarVisible,
|
||||
setSideBarVisible
|
||||
}}>
|
||||
<div
|
||||
id='theme-game'
|
||||
className={`${siteConfig('FONT_STYLE')} w-full h-full min-h-screen justify-center dark:text-gray-300 scroll-smooth`}>
|
||||
<Style />
|
||||
{/* 左右布局 */}
|
||||
<div id='wrapper' className={'relative flex justify-between w-full h-full mx-auto'}>
|
||||
{/* 左侧 */}
|
||||
<div className='w-52 hidden xl:block relative z-10'>
|
||||
<div className='py-4 px-2 sticky top-0 h-screen flex flex-col justify-between'>
|
||||
{/* 顶部 */}
|
||||
|
||||
<div className=''>
|
||||
<Header />
|
||||
<NavBar />
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<AdSlot />
|
||||
<AdSlot />
|
||||
<AdSlot />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧 */}
|
||||
<main className='flex-grow w-full overflow-x-scroll'>
|
||||
{children}
|
||||
<div className='ads w-full justify-center flex p-2'>
|
||||
<AdSlot type='flow' />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<SideBarDrawer
|
||||
isOpen={sideBarVisible}
|
||||
onClose={() => {
|
||||
setSideBarVisible(false)
|
||||
}}>
|
||||
<SideBarContent />
|
||||
</SideBarDrawer>
|
||||
</div>
|
||||
</ThemeGlobalGame.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* 首页是个博客列表,加上顶部嵌入一个公告
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutIndex = props => {
|
||||
return <LayoutPostList {...props} topSlot={<Announcement {...props} />} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 博客列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutPostList = props => {
|
||||
const { posts, topSlot, tag } = props
|
||||
const { filterKey } = useGameGlobal()
|
||||
let filteredBlogPosts = []
|
||||
if (filterKey && posts) {
|
||||
filteredBlogPosts = posts.filter(post => {
|
||||
const tagContent = post?.tags ? post?.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(filterKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredBlogPosts = deepClone(posts)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{topSlot}
|
||||
{tag && <SearchNavBar {...props} />}
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||
<BlogListPage {...props} posts={filteredBlogPosts} />
|
||||
) : (
|
||||
<BlogListScroll {...props} posts={filteredBlogPosts} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* 页面是博客列表,上方嵌入一个搜索引导条
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSearch = props => {
|
||||
const { keyword, posts } = props
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
replaceSearchResult({
|
||||
doms: document.getElementById('posts-wrapper'),
|
||||
search: keyword,
|
||||
target: {
|
||||
element: 'span',
|
||||
className: 'text-red-500 border-b border-dashed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 在列表中进行实时过滤
|
||||
const { filterKey } = useGameGlobal()
|
||||
let filteredBlogPosts = []
|
||||
if (filterKey && posts) {
|
||||
filteredBlogPosts = posts.filter(post => {
|
||||
const tagContent = post?.tags ? post?.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(filterKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredBlogPosts = deepClone(posts)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchNavBar {...props} />
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||
<BlogListPage {...props} posts={filteredBlogPosts} />
|
||||
) : (
|
||||
<BlogListScroll {...props} posts={filteredBlogPosts} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档
|
||||
* @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 => (
|
||||
<BlogArchiveItem key={archiveTitle} archiveTitle={archiveTitle} archivePosts={archivePosts} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情
|
||||
* @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} />}
|
||||
|
||||
{!lock && (
|
||||
<div id='article-wrapper' className='px-2'>
|
||||
<>
|
||||
<ArticleInfo post={post} />
|
||||
<NotionPage post={post} />
|
||||
<ShareBar post={post} />
|
||||
<Comment frontMatter={post} />
|
||||
<ArticleFooter />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 页面
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Layout404 = props => {
|
||||
return <>404 Not found.</>
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章分类列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutCategoryIndex = props => {
|
||||
const { categoryOptions } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categoryOptions?.map(category => {
|
||||
return (
|
||||
<Link key={category.name} href={`/category/${category.name}`} passHref legacyBehavior>
|
||||
<div
|
||||
className={
|
||||
'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
|
||||
}>
|
||||
<i className='mr-4 fas fa-folder' />
|
||||
{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章标签列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutTagIndex = props => {
|
||||
const { tagOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{tagOptions.map(tag => {
|
||||
return (
|
||||
<div key={tag.name} className='p-2'>
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
|
||||
<div className='font-light dark:text-gray-400'>
|
||||
<i className='mr-1 fas fa-tag' /> {tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Layout404,
|
||||
LayoutArchive,
|
||||
LayoutBase,
|
||||
LayoutCategoryIndex,
|
||||
LayoutIndex,
|
||||
LayoutPostList,
|
||||
LayoutSearch,
|
||||
LayoutSlug,
|
||||
LayoutTagIndex,
|
||||
CONFIG as THEME_CONFIG
|
||||
}
|
||||
18
themes/game/style.js
Normal file
18
themes/game/style.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
/**
|
||||
* 此处样式只对当前主题生效
|
||||
* 此处不支持tailwindCSS的 @apply 语法
|
||||
* @returns
|
||||
*/
|
||||
const Style = () => {
|
||||
return <style jsx global>{`
|
||||
|
||||
// 底色
|
||||
.dark body{
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
}
|
||||
|
||||
export { Style }
|
||||
Reference in New Issue
Block a user