diff --git a/themes/game/components/AdBlockerDetect.js b/themes/game/components/AdBlockerDetect.js new file mode 100644 index 00000000..81f24d39 --- /dev/null +++ b/themes/game/components/AdBlockerDetect.js @@ -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 ( + <> +
+
+
+

+ Please allow ads on our site +

+
+
+
+

+ { + "Looks like you're using an ad blocker. We rely on advertising to help fund our site." + } +

+
+
+ +
+
+
+ + ) +} diff --git a/themes/game/components/Announcement.js b/themes/game/components/Announcement.js new file mode 100644 index 00000000..088c412e --- /dev/null +++ b/themes/game/components/Announcement.js @@ -0,0 +1,18 @@ +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +const Announcement = ({ notice, className }) => { + if (notice?.blockMap) { + return
+
+ {notice && (
+ +
)} +
+
+ } else { + return null + } +} +export default Announcement diff --git a/themes/game/components/ArticleFooter.js b/themes/game/components/ArticleFooter.js new file mode 100644 index 00000000..628dd05d --- /dev/null +++ b/themes/game/components/ArticleFooter.js @@ -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
+ + + + + + +
+} diff --git a/themes/game/components/ArticleInfo.js b/themes/game/components/ArticleInfo.js new file mode 100644 index 00000000..b916d8cc --- /dev/null +++ b/themes/game/components/ArticleInfo.js @@ -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
+
+ +

+ {post?.title} +

+ + {post?.type !== 'Page' && <> + + } + +
+ +
+} diff --git a/themes/game/components/ArticleLock.js b/themes/game/components/ArticleLock.js new file mode 100644 index 00000000..3a5f4b4c --- /dev/null +++ b/themes/game/components/ArticleLock.js @@ -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 = `
${locale.COMMON.PASSWORD_ERROR}
` + } + } + } + + const passwordInputRef = useRef(null) + useEffect(() => { + // 选中密码输入框并将其聚焦 + passwordInputRef.current.focus() + }, []) + + return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+ { + 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' + > +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+
+} diff --git a/themes/game/components/BlogArchiveItem.js b/themes/game/components/BlogArchiveItem.js new file mode 100644 index 00000000..ac1c2522 --- /dev/null +++ b/themes/game/components/BlogArchiveItem.js @@ -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 ( +
+
+ {archiveTitle} +
+ + +
+ ) +} diff --git a/themes/game/components/BlogListBar.js b/themes/game/components/BlogListBar.js new file mode 100644 index 00000000..74511a89 --- /dev/null +++ b/themes/game/components/BlogListBar.js @@ -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 ( +
+
+ handleSearchChange(e.target.value)} + /> + + + +
+ +
+ ) + } else { + return <> + } +} diff --git a/themes/game/components/BlogListPage.js b/themes/game/components/BlogListPage.js new file mode 100644 index 00000000..46db49fa --- /dev/null +++ b/themes/game/components/BlogListPage.js @@ -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 ( +
+ +
+ {posts?.map(post => ( + + ))} +
+ +
+ + + + + + + + + + +
+
+ ) +} diff --git a/themes/game/components/BlogListScroll.js b/themes/game/components/BlogListScroll.js new file mode 100644 index 00000000..42a1f440 --- /dev/null +++ b/themes/game/components/BlogListScroll.js @@ -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 ( +
+ {postsToShow.map(p => ( + + ))} + +
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+ +
+ ) +} diff --git a/themes/game/components/BlogPost.js b/themes/game/components/BlogPost.js new file mode 100644 index 00000000..b5845cf9 --- /dev/null +++ b/themes/game/components/BlogPost.js @@ -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 ( + ( + +
+
+

+ {post.title} +

+ +
+
+ {!showPreview &&

+ {post.summary} +

} + {showPreview && post?.blockMap && ( +
+ +
+
+ )} +
+
+ + ) + ) +} + +export default BlogPost diff --git a/themes/game/components/Draggable.js b/themes/game/components/Draggable.js new file mode 100644 index 00000000..bd80e562 --- /dev/null +++ b/themes/game/components/Draggable.js @@ -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
+ {children} +
+} + +Draggable.defaultProps = { left: 0, top: 0 } diff --git a/themes/game/components/ExampleRecentComments.js b/themes/game/components/ExampleRecentComments.js new file mode 100644 index 00000000..9dbdfa7f --- /dev/null +++ b/themes/game/components/ExampleRecentComments.js @@ -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 &&
Loading...
} + {!onLoading && comments && comments.length === 0 &&
No Comments
} + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
--{comment.nick}
+
)} + + +} + +export default ExampleRecentComments diff --git a/themes/game/components/Footer.js b/themes/game/components/Footer.js new file mode 100644 index 00000000..a5d44e27 --- /dev/null +++ b/themes/game/components/Footer.js @@ -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
+ +
+
+
+

+ © {siteConfig('AUTHOR')} {copyrightDate} +

+ +
+
+
+} diff --git a/themes/game/components/FullScreen.js b/themes/game/components/FullScreen.js new file mode 100644 index 00000000..1fcda710 --- /dev/null +++ b/themes/game/components/FullScreen.js @@ -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 ( +
+ full screen + + FullScreen + +
+ ) +} diff --git a/themes/game/components/GameListIndexCombine.js b/themes/game/components/GameListIndexCombine.js new file mode 100644 index 00000000..2d8f4f91 --- /dev/null +++ b/themes/game/components/GameListIndexCombine.js @@ -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 + 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() + continue + } + + // 试图将4合一卡组塞满 + while (gamesClone?.length > 0 && groupItems.length < 4) { + const item = gamesClone.shift() + if (item.recommend) { + components.push() + break + } else { + groupItems.push(item) + } + } + + if (groupItems.length === 4 || (gamesClone.length === 0 && groupItems.length > 4)) { + components.push() + groupItems = [] + } else { + while (groupItems.length > 0) { + const item = groupItems.shift() + components.push() + } + } + } + } else { + while (gamesClone?.length > 0) { + index++ + + if (index % 6 === 0) { + components.push() + } else if (index % 2 === 0 && gamesClone?.length >= 4) { + // 如果是偶数,则从游戏列表中退出4个组成大卡牌 + const groupItems = [] + for (let i = 1; i <= 4; i++) { + groupItems.push(gamesClone.shift()) + } + components.push() + } else { + const item = gamesClone.shift() + components.push() + } + } + } + + return ( +
+
+ {components?.map((ItemComponent, index) => { + return ItemComponent + })} +
+
+ ) +} + +/** + * 一个广告游戏大卡 + * @returns + */ +const GameAd = () => { + return ( +
+ +
+ ) +} + +/** + * 4卡组成一个大卡 + * @param {*} param0 + * @returns + */ +const GameItemGroup = ({ items }) => { + return ( +
+ {items.map((item, index) => ( + + ))} +
+ ) +} + +/** + * 游戏=单卡 + * @param {*} param0 + * @returns + */ +const GameItem = ({ item, isLargeCard }) => { + const { id, title, img, video } = item + const [showType, setShowType] = useState('img') // img or video + return ( + { + 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`}> +
+ {title} +
+
+
+
+ + {showType === 'video' && ( + + )} + {title} +
+ ) +} diff --git a/themes/game/components/GameListNormal.js b/themes/game/components/GameListNormal.js new file mode 100644 index 00000000..9e048c7b --- /dev/null +++ b/themes/game/components/GameListNormal.js @@ -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 + const components = [] + + let index = 0 + // 无限循环 + while (gamesClone?.length > 0 && index < maxCount) { + const item = gamesClone.shift() + components.push() + index++ + continue + } + + return ( +
+
+ {components?.map((ItemComponent, index) => { + return ItemComponent + })} +
+
+ ) +} + +/** + * 游戏=单卡 + * @param {*} param0 + * @returns + */ +const GameItem = ({ item }) => { + const { id, title, img, video } = item + const [showType, setShowType] = useState('img') // img or video + + return ( + { + 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`}> +
{title}
+
+
+
+ + {showType === 'video' && ( + + )} + {title} +
+ ) +} diff --git a/themes/game/components/GameListRealate.js b/themes/game/components/GameListRealate.js new file mode 100644 index 00000000..f2ed7376 --- /dev/null +++ b/themes/game/components/GameListRealate.js @@ -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 + const components = [] + const maxCount = 24 + + let index = 0 + // 无限循环 + while (gamesClone?.length > 0 && index < maxCount) { + const item = gamesClone.shift() + components.push() + index++ + continue + } + + return ( +
+
+ {components?.map((ItemComponent, index) => { + return ItemComponent + })} +
+
+ ) +} + +/** + * 游戏=单卡 + * @param {*} param0 + * @returns + */ +const GameItem = ({ item, isLargeCard }) => { + const { id, title, img, video } = item + const [showType, setShowType] = useState('img') // img or video + + return ( + { + 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`}> +
+ {title} +
+
+
+
+ + {showType === 'video' && ( + + )} + {title} +
+ ) +} diff --git a/themes/game/components/GameListRecent.js b/themes/game/components/GameListRecent.js new file mode 100644 index 00000000..582ed439 --- /dev/null +++ b/themes/game/components/GameListRecent.js @@ -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 + const components = [] + + let index = 0 + // 无限循环 + while (gamesClone?.length > 0 && index < maxCount) { + const item = gamesClone?.shift() + if (item) { + components.push() + index++ + } + continue + } + + if (components.length === 0) { + return <> + } + + return ( + <> +
Recent Played
+
+
+ {components?.map((ItemComponent, index) => { + return ItemComponent + })} +
+
+ + ) +} + +/** + * 游戏=单卡 + * @param {*} param0 + * @returns + */ +const GameItem = ({ item }) => { + const { id, title, img, video } = item || {} + const [showType, setShowType] = useState('img') // img or video + + return ( + { + 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`}> +
{title}
+
+
+
+ + {showType === 'video' && ( + + )} + {title} +
+ ) +} diff --git a/themes/game/components/Header.js b/themes/game/components/Header.js new file mode 100644 index 00000000..0e5297a5 --- /dev/null +++ b/themes/game/components/Header.js @@ -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 ( +
+
+ + + +
+
+ ) +} diff --git a/themes/game/components/JumpToTopButton.js b/themes/game/components/JumpToTopButton.js new file mode 100644 index 00000000..f5e22b61 --- /dev/null +++ b/themes/game/components/JumpToTopButton.js @@ -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
window.scrollTo({ top: 0, behavior: 'smooth' })} + > +
+} + +export default JumpToTopButton diff --git a/themes/game/components/Logo.js b/themes/game/components/Logo.js new file mode 100644 index 00000000..da416169 --- /dev/null +++ b/themes/game/components/Logo.js @@ -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 ( + +
+

{siteConfig('TITLE')}

+

{siteConfig('BIO')}

+
+ + ) +} diff --git a/themes/game/components/LogoMini.js b/themes/game/components/LogoMini.js new file mode 100644 index 00000000..b540e6e6 --- /dev/null +++ b/themes/game/components/LogoMini.js @@ -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 ( + + {siteConfig('TITLE')?.charAt(0)} + + ) +} diff --git a/themes/game/components/MenuItemCollapse.js b/themes/game/components/MenuItemCollapse.js new file mode 100644 index 00000000..8f53f622 --- /dev/null +++ b/themes/game/components/MenuItemCollapse.js @@ -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 <> +
+ {!hasSubMenu && + {link?.icon && }{link?.name} + } + {hasSubMenu &&
+ {link?.icon && }{link?.name} + +
} +
+ + {/* 折叠子菜单 */} + {hasSubMenu && + {link.subMenus.map((sLink, index) => { + return
+ + {sLink.title} + +
+ })} +
} + +} diff --git a/themes/game/components/MenuItemDrop.js b/themes/game/components/MenuItemDrop.js new file mode 100644 index 00000000..0386910c --- /dev/null +++ b/themes/game/components/MenuItemDrop.js @@ -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
  • +
    changeShow(true)} onMouseOut={() => changeShow(false)}> + {!hasSubMenu && +
    + + {link?.icon && } {link?.name} + +
    + } + + {hasSubMenu && +
    + {link?.icon && } {link?.name} + +
    + } + + {/* 子菜单 */} + {hasSubMenu &&
      + {link.subMenus.map((sLink, index) => { + return
      + + {link?.icon &&   }{sLink.title} + +
      + })} +
    } + +
    + +
  • +} diff --git a/themes/game/components/MenuList.js b/themes/game/components/MenuList.js new file mode 100644 index 00000000..83420700 --- /dev/null +++ b/themes/game/components/MenuList.js @@ -0,0 +1,30 @@ +import Link from 'next/link' +import { useGameGlobal } from '..' + +export const MenuList = () => { + const { setSideBarVisible } = useGameGlobal() + return ( +
    +
      +
    • + + + + Home + + +
    • +
    • + +
    • +
    +
    + ) +} diff --git a/themes/game/components/Nav.js b/themes/game/components/Nav.js new file mode 100644 index 00000000..04b1e9fb --- /dev/null +++ b/themes/game/components/Nav.js @@ -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 ( + <> +
    + + + ) +} + +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 ( +
    +
      + {links?.map((link, index) => ( + + ))} +
    +
    + +
    + {links?.map((link, index) => ( + collapseRef.current?.updateCollapseHeight(param)} + /> + ))} +
    +
    +
    + + {JSON.parse(siteConfig('GAME_MENU_RANDOM_POST', null, CONFIG)) && } + {JSON.parse(siteConfig('GAME_MENU_SEARCH_BUTTON', null, CONFIG)) && } + +
    + ) +} + +export default Nav diff --git a/themes/game/components/NavBar.js b/themes/game/components/NavBar.js new file mode 100644 index 00000000..0a146c40 --- /dev/null +++ b/themes/game/components/NavBar.js @@ -0,0 +1,9 @@ +import { MenuList } from './MenuList' + +export default function NavBar({ className }) { + return ( + + ) +} diff --git a/themes/game/components/RandomPostButton.js b/themes/game/components/RandomPostButton.js new file mode 100644 index 00000000..a791f0f8 --- /dev/null +++ b/themes/game/components/RandomPostButton.js @@ -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 ( +
    + +
    + ) +} diff --git a/themes/game/components/SearchButton.js b/themes/game/components/SearchButton.js new file mode 100644 index 00000000..ea8fa508 --- /dev/null +++ b/themes/game/components/SearchButton.js @@ -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 ( + <> +
    + +
    + + ) +} diff --git a/themes/game/components/SearchInput.js b/themes/game/components/SearchInput.js new file mode 100644 index 00000000..9f1ffdbe --- /dev/null +++ b/themes/game/components/SearchInput.js @@ -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
    + updateSearchKey(e.target.value)} + defaultValue={keyword || ''} + /> + +
    + +
    + + {(showClean && +
    + +
    + )} +
    +} + +export default SearchInput diff --git a/themes/game/components/SearchNavBar.js b/themes/game/components/SearchNavBar.js new file mode 100644 index 00000000..79557cff --- /dev/null +++ b/themes/game/components/SearchNavBar.js @@ -0,0 +1,17 @@ +import SearchInput from './SearchInput' +import Tags from './Tags' + +/** + * 搜索页面上方嵌入内容 + * @param {*} props + * @returns + */ +export default function SearchNavBar(props) { + return (<> +
    + +
    + + + ) +} diff --git a/themes/game/components/SideBar.js b/themes/game/components/SideBar.js new file mode 100644 index 00000000..a4d1e060 --- /dev/null +++ b/themes/game/components/SideBar.js @@ -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 ( +
    + + + + + + {siteConfig('COMMENT_WALINE_SERVER_URL') && JSON.parse(siteConfig('COMMENT_WALINE_RECENT')) && } + + + +
    + ); +} diff --git a/themes/game/components/SideBarContent.js b/themes/game/components/SideBarContent.js new file mode 100644 index 00000000..1960b63f --- /dev/null +++ b/themes/game/components/SideBarContent.js @@ -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 ( +
    +
    + + +
    + +
    + +
    +
    + ) +} diff --git a/themes/game/components/SideBarDrawer.js b/themes/game/components/SideBarDrawer.js new file mode 100644 index 00000000..a3f64924 --- /dev/null +++ b/themes/game/components/SideBarDrawer.js @@ -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 ( +