Merge pull request #2214 from tangly1024/feat/theme-game

Feat/theme game
This commit is contained in:
tangly1024
2024-03-24 20:58:20 +08:00
committed by GitHub
69 changed files with 3076 additions and 417 deletions

View File

@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=4.4.0
NEXT_PUBLIC_VERSION=4.4.1
# 可在此添加环境变量,去掉最左边的(# )注释即可

View File

@@ -1,10 +1,10 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"arrowParens": "avoid",
"printWidth": 120,
"bracketSpacing": true,
"jsxSingleQuote": true,
"jsxBracketSameLine": true
}
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"arrowParens": "avoid",
"printWidth": 80,
"bracketSpacing": true,
"jsxSingleQuote": true,
"jsxBracketSameLine": true
}

View File

@@ -1,8 +1,7 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_idDuplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_PAGE_ID:
process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
NOTION_PAGE_ID: process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径开启后所有文章URL都以 .html 结尾。
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒)即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据调大该值有助于节省Vercel资源、同时提升访问速率但也会使文章更新有延迟。
THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题在themes文件夹下可找到所有支持的主题主题名称就是文件夹名例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
@@ -15,7 +14,9 @@ const BLOG = {
IS_TAG_COLOR_DISTINGUISHED: process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
// 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || 'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉',
GREETING_WORDS:
process.env.NEXT_PUBLIC_GREETING_WORDS ||
'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉',
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型从3.12.0版本起各主题将逐步支持灵活的二级菜单配置替代了原来的Page类型此配置是试验功能、默认关闭。
@@ -90,7 +91,9 @@ const BLOG = {
'"Segoe UI Symbol"',
'"Apple Color Emoji"'
],
FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
FONT_AWESOME:
process.env.NEXT_PUBLIC_FONT_AWESOME_PATH ||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
// END ************网站字体*****************
@@ -120,10 +123,13 @@ const BLOG = {
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。
// 自定义右键菜单
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE || true, // 是否显示深色模式
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY || true, // 是否显示分类
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_TAG || true, // 是否显示标签
@@ -148,10 +154,16 @@ const BLOG = {
PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
// 代码主题 @see https://github.com/PrismJS/prism-themes
PRISM_THEME_PREFIX_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
PRISM_THEME_PREFIX_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
PRISM_THEME_SWITCH: process.env.NEXT_PUBLIC_PRISM_THEME_SWITCH || true, // 是否开启浅色/深色模式代码主题切换; 开启后将显示以下两个主题
PRISM_THEME_LIGHT_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
PRISM_THEME_DARK_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
PRISM_THEME_LIGHT_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
PRISM_THEME_DARK_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
@@ -161,16 +173,20 @@ const BLOG = {
// END********代码相关********
// Mermaid 图表CDN
MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
MERMAID_CDN:
process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
// QRCodeCDN
QR_CODE_CDN: process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
QR_CODE_CDN:
process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
SUB_PATH: '', // leave this empty unless you want to deploy in a folder
POST_SHARE_BAR_ENABLE: process.env.NEXT_PUBLIC_POST_SHARE_BAR || 'true', // 文章分享功能 ,将在底部显示一个分享条
POSTS_SHARE_SERVICES: process.env.NEXT_PUBLIC_POST_SHARE_SERVICES || 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
POSTS_SHARE_SERVICES:
process.env.NEXT_PUBLIC_POST_SHARE_SERVICES ||
'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
// 所有支持的分享服务link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
@@ -181,9 +197,9 @@ const BLOG = {
POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
POST_PREVIEW_LINES: 12, // 预览博客行数
POST_RECOMMEND_COUNT: 6, // 推荐文章数量
POSTS_PER_PAGE: 12, // post counts per page
POST_PREVIEW_LINES: process.env.NEXT_PUBLIC_POST_POST_PREVIEW_LINES || 12, // 预览博客行数
POST_RECOMMEND_COUNT: process.env.NEXT_PUBLIC_POST_RECOMMEND_COUNT || 6, // 推荐文章数量
POSTS_PER_PAGE: process.env.NEXT_PUBLIC_POST_PER_PAGE || 12, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
POST_WAITING_TIME_FOR_404: process.env.NEXT_PUBLIC_POST_WAITING_TIME_FOR_404 || '8', // 文章加载超时时间单位秒超时后跳转到404页面
@@ -203,12 +219,7 @@ const BLOG = {
// 鼠标点击烟花特效
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
FIREWORKS_COLOR: [
'255, 20, 97',
'24, 255, 146',
'90, 135, 255',
'251, 243, 140'
],
FIREWORKS_COLOR: ['255, 20, 97', '24, 255, 146', '90, 135, 255', '251, 243, 140'],
// 樱花飘落特效
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
@@ -223,8 +234,11 @@ const BLOG = {
// ********挂件组件相关********
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
TianliGPT_CSS: process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
TianliGPT_JS: process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
TianliGPT_CSS:
process.env.NEXT_PUBLIC_TIANLI_GPT_CSS ||
'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
TianliGPT_JS:
process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
// Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
@@ -239,19 +253,18 @@ const BLOG = {
// 悬浮挂件
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
WIDGET_PET_LINK:
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
// 音乐播放插件
MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
MUSIC_PLAYER_AUTO_PLAY:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
MUSIC_PLAYER_AUTO_PLAY: process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url前提是有配置歌词路径对 meting 无效)
MUSIC_PLAYER_CDN_URL:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list随机 random
MUSIC_PLAYER_AUDIO_LIST: [
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
@@ -259,24 +272,19 @@ const BLOG = {
name: '风を共に舞う気持ち',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
cover:
'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
cover: 'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
},
{
name: '王都グランセル',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
cover:
'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
cover: 'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
}
],
MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST更多配置信息https://github.com/metowolf/MetingJS
MUSIC_PLAYER_METING_SERVER:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
MUSIC_PLAYER_METING_ID:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
MUSIC_PLAYER_METING_LRC_TYPE:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
MUSIC_PLAYER_METING_SERVER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
MUSIC_PLAYER_METING_ID: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
MUSIC_PLAYER_METING_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
@@ -285,55 +293,47 @@ const BLOG = {
// artalk 评论插件
COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
COMMENT_ARTALK_JS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
COMMENT_ARTALK_CSS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
COMMENT_ARTALK_JS:
process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
COMMENT_ARTALK_CSS:
process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
// twikoo
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envIdVercel环境填域名教程https://tangly1024.com/article/notionnext-twikoo
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
COMMENT_TWIKOO_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
// utterance
COMMENT_UTTERRANCES_REPO:
process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext' 更多文档参考 https://utteranc.es/
COMMENT_UTTERRANCES_REPO: process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext' 更多文档参考 https://utteranc.es/
// giscus @see https://giscus.app/
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_CATEGORY_ID:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_MAPPING:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
COMMENT_GISCUS_REACTIONS_ENABLED:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
COMMENT_GISCUS_EMIT_METADATA:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
COMMENT_GISCUS_INPUT_POSITION:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_MAPPING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
COMMENT_GISCUS_REACTIONS_ENABLED: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
COMMENT_GISCUS_EMIT_METADATA: process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
COMMENT_GISCUS_INPUT_POSITION: process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
COMMENT_GISCUS_LOADING:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
COMMENT_GISCUS_CROSSORIGIN:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_GISCUS_LOADING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
COMMENT_GISCUS_CROSSORIGIN: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
COMMENT_CUSDIS_HOST:
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
COMMENT_CUSDIS_SCRIPT_SRC:
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
'/js/cusdis.es.js', // change this if you're using self-hosted version
COMMENT_CUSDIS_HOST: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
COMMENT_CUSDIS_SCRIPT_SRC: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC || '/js/cusdis.es.js', // change this if you're using self-hosted version
// gitalk评论插件 更多参考 https://gitalk.github.io/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名例如 'NotionNext'
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
COMMENT_GITALK_CLIENT_ID:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_SECRET:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_ID: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID 在gitalk后台获取
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
COMMENT_GITALK_JS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
COMMENT_GITALK_CSS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITALK_JS_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
COMMENT_GITALK_CSS_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
@@ -343,8 +343,7 @@ const BLOG = {
COMMENT_VALINE_APP_ID: process.env.NEXT_PUBLIC_VALINE_ID || '', // Valine @see https://valine.js.org/quickstart.html 或 https://github.com/stonehank/react-valine#%E8%8E%B7%E5%8F%96app-id-%E5%92%8C-app-key
COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
COMMENT_VALINE_PLACEHOLDER:
process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
COMMENT_VALINE_PLACEHOLDER: process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
@@ -383,11 +382,9 @@ const BLOG = {
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash
ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '82e51db6-dec2-423a-b7c9-b4ff7ebb3302'
SEO_GOOGLE_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_GOOGLE_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_BAIDU_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_BAIDU_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
// 微软 Clarity 站点分析
CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分ID是一个十位的英文数字组合
@@ -416,23 +413,20 @@ const BLOG = {
type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时为博文。
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时为单页。
type_notice:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时为公告。
type_notice: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时为公告。
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时为菜单。
type_sub_menu:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时为子菜单。
type_sub_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时为子菜单。
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
status_publish:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文
status_invisible:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布可以为中文 除此之外其他页面状态不会显示在博客上
status_publish: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文
status_invisible: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 除此之外其他页面状态不会显示在博客上
summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon',
ext: process.env.NEXT_PUBLIC_NOTION_PROPERTY_EXT || 'ext' // 扩展字段存放json-string用于复杂业务
},
// RSS订阅
@@ -441,10 +435,14 @@ const BLOG = {
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY || null, // 开启mailichimp邮件订阅 APIkey
// ANIMATE.css 动画
ANIMATE_CSS_URL: process.env.NEXT_PUBLIC_ANIMATE_CSS_URL || 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css', // 动画CDN
ANIMATE_CSS_URL:
process.env.NEXT_PUBLIC_ANIMATE_CSS_URL ||
'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css', // 动画CDN
// 网站图片
IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址支持base64或url
IMG_LAZY_LOAD_PLACEHOLDER:
process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER ||
'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址支持base64或url
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效请勿使用AMAZON方案不再支持仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
IMG_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMG_COMPRESS_WIDTH || 800, // Notion图片压缩宽度
@@ -452,15 +450,16 @@ const BLOG = {
// 作废配置
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 被notion中的页面标题覆盖此处请勿留空白否则服务器无法编译
HOME_BANNER_IMAGE:
process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖若无封面图则会使用代码中的 /public/bg_image.jpg 文件
DESCRIPTION:
process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖
HOME_BANNER_IMAGE: process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖若无封面图则会使用代码中的 /public/bg_image.jpg 文件
DESCRIPTION: process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖
// 开发相关
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
ENABLE_CACHE:
process.env.ENABLE_CACHE ||
process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号

View File

@@ -1,10 +1,10 @@
import { useRef, useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
/**
* 可拖拽组件
*/
export const Draggable = (props) => {
const { children } = props
export const Draggable = props => {
const { children, stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
@@ -14,8 +14,10 @@ export const Draggable = (props) => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
function e(event) { // 定义事件对象标准化函数
if (!event) { // 兼容IE浏览器
function e(event) {
// 定义事件对象标准化函数
if (!event) {
// 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
@@ -40,9 +42,10 @@ export const Draggable = (props) => {
document.onmousedown = start
document.ontouchstart = start
function start (event) { // 按下鼠标时,初始化处理
function start(event) {
// 按下鼠标时,初始化处理
if (!draggableElements) return
event = e(event)// 获取标准事件对象
event = e(event) // 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
@@ -60,19 +63,20 @@ export const Draggable = (props) => {
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
document.onmousemove = move// 注册鼠标移动事件处理函数
document.onmousemove = move // 注册鼠标移动事件处理函数
document.ontouchmove = move
document.onmouseup = stop// 注册松开鼠标事件处理函数
document.onmouseup = stop // 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
function move(event) { // 鼠标移动处理函数
function move(event) {
// 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
const stop = (event) => {
const stop = event => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
@@ -80,7 +84,7 @@ export const Draggable = (props) => {
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
const updatePosition = (event) => {
const updatePosition = event => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
@@ -120,15 +124,18 @@ export const Draggable = (props) => {
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
if (offsetTop > (clientHeight - offsetHeight)) {
if (offsetTop > clientHeight - offsetHeight) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
if (offsetLeft > (clientWidth - offsetWidth)) {
if (offsetLeft > clientWidth - offsetWidth) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
if (stick === 'left') {
drag.firstElementChild.style.left = 0 + 'px'
}
}
}
@@ -142,9 +149,11 @@ export const Draggable = (props) => {
}
}, [])
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
return (
<div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
)
}
Draggable.defaultProps = { left: 0, top: 0 }

View File

@@ -17,7 +17,7 @@ import { deepClone } from './utils'
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
let global = null
try {
const isClient = typeof window !== 'undefined';
const isClient = typeof window !== 'undefined'
// eslint-disable-next-line react-hooks/rules-of-hooks
global = isClient ? useGlobal() : {}
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -67,15 +67,19 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
} else {
if (typeof val === 'string') {
if (val === 'true' || val === 'false') {
return JSON.parse(val);
return JSON.parse(val)
}
return val;
if (/^\d+$/.test(val)) {
// 如果是数字使用parseFloat或者parseInt将字符串转换为数字
return parseInt(val)
}
return val
} else {
try {
return JSON.parse(val);
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val;
return val
}
}
}

View File

@@ -1,18 +1,18 @@
import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
import { idToUuid } from 'notion-utils'
import { deepClone } from '@/lib/utils'
import { getAllCategories } from '@/lib/notion/getAllCategories'
import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags'
import getPageProperties from '@/lib/notion/getPageProperties'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
import getPageProperties from '@/lib/notion/getPageProperties'
import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
export { getAllTags } from '../notion/getAllTags'
export { getPostBlocks } from '../notion/getPostBlocks'
export { getPost } from '../notion/getNotionPost'
export { getPostBlocks } from '../notion/getPostBlocks'
/**
* 获取博客数据; 基于Notion实现
@@ -275,7 +275,8 @@ export function getNavPages({ allPages }) {
slug: item.slug,
pageIcon: item.pageIcon || '',
lastEditedDate: item.lastEditedDate,
publishDate: item.publishDate
publishDate: item.publishDate,
ext: item.ext || {}
}))
}

View File

@@ -1,6 +1,6 @@
import { getTextContent, getDateValue } from 'notion-utils'
import { NotionAPI } from 'notion-client'
import BLOG from '@/blog.config'
import { NotionAPI } from 'notion-client'
import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto'
import md5 from 'js-md5'
@@ -49,8 +49,7 @@ export default async function getPageProperties(id, value, schema, authToken, ta
if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0]
const res = await api.getUsers(userId)
const resValue =
res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const resValue = res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = {
id: resValue?.id,
first_name: resValue?.given_name,
@@ -93,16 +92,17 @@ export default async function getPageProperties(id, value, schema, authToken, ta
properties.pageIcon = mapImgUrl(value?.format?.page_icon, value) ?? ''
properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? ''
properties.pageCoverThumbnail = mapImgUrl(value?.format?.page_cover, value, 'block', 'pageCoverThumbnail') ?? ''
properties.ext = converToJSON(properties?.ext)
properties.content = value.content ?? []
properties.tagItems = properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
properties.tagItems =
properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
delete properties.content
// 处理URL
if (properties.type === 'Post') {
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
properties.slug = BLOG.POST_URL_PREFIX ? generateCustomizeUrl(properties) : properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.slug = properties.slug ?? properties.id
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
@@ -122,6 +122,24 @@ export default async function getPageProperties(id, value, schema, authToken, ta
return properties
}
/**
* 字符串转json
* @param {*} str
* @returns
*/
function converToJSON(str) {
if (!str) {
return {}
}
// 使用正则表达式去除空格和换行符
try {
return JSON.parse(str.replace(/\s/g, ''))
} catch (error) {
console.warn('无效JSON', str)
return {}
}
}
/**
* 映射用户自定义表头
*/
@@ -164,7 +182,7 @@ function generateCustomizeUrl(postProperties) {
const formatPostCreatedDate = new Date(postProperties?.publishDay)
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
} else if (pattern === '%slug%') {
fullPrefix += (postProperties.slug ?? postProperties.id)
fullPrefix += postProperties.slug ?? postProperties.id
} else if (!pattern.includes('%')) {
fullPrefix += pattern
} else {
@@ -180,5 +198,5 @@ function generateCustomizeUrl(postProperties) {
if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
return `${fullPrefix}/${(postProperties.slug ?? postProperties.id)}`
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}

View File

@@ -1,5 +1,5 @@
// 封装异步加载资源的方法
import { memo } from 'react'
import { memo } from 'react';
/**
* 判断是否客户端
@@ -7,6 +7,22 @@ import { memo } from 'react'
*/
export const isBrowser = typeof window !== 'undefined'
/**
* 打乱数组
* @param {*} array
* @returns
*/
export const shuffleArray = (array) => {
if (!array) {
return []
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* google机器人
* @returns

View File

@@ -1,6 +1,6 @@
{
"name": "notion-next",
"version": "4.4.0",
"version": "4.4.1",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {

View File

@@ -1,9 +1,10 @@
import BLOG from '@/blog.config'
import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -12,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
return <Slug {...props}/>
return <Slug {...props} />
}
/**
@@ -31,8 +32,11 @@ export async function getStaticPaths() {
const { allPages } = await getGlobalData({ from })
return {
paths: allPages?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) } })),
paths: allPages
?.filter(row => checkSlug(row))
.map(row => ({
params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) }
})),
fallback: true
}
}
@@ -52,8 +56,8 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -86,7 +90,7 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null

View File

@@ -1,9 +1,10 @@
import BLOG from '@/blog.config'
import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -12,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
return <Slug {...props}/>
return <Slug {...props} />
}
export async function getStaticPaths() {
@@ -25,7 +26,8 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
const paths = allPages?.filter(row => checkSlug(row))
const paths = allPages
?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } }))
return {
paths: paths,
@@ -43,8 +45,8 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -77,7 +79,7 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null

View File

@@ -1,14 +1,14 @@
import BLOG from '@/blog.config'
import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
import { useEffect, useState } from 'react'
import { idToUuid } from 'notion-utils'
import { useRouter } from 'next/router'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5'
import { checkContainHttp } from '@/lib/utils'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { siteConfig } from '@/lib/config'
import { useRouter } from 'next/router'
import { idToUuid } from 'notion-utils'
import { useEffect, useState } from 'react'
/**
* 根据notion的slug访问页面
@@ -25,7 +25,7 @@ const Slug = props => {
/**
* 验证文章密码
* @param {*} result
*/
*/
const validPassword = passInput => {
const encrypt = md5(post.slug + passInput)
if (passInput && encrypt === post.password) {
@@ -43,7 +43,9 @@ const Slug = props => {
} else {
setLock(false)
if (!lock && post?.blockMap?.block) {
post.content = Object.keys(post.blockMap.block).filter(key => post.blockMap.block[key]?.value?.parent_id === post.id)
post.content = Object.keys(post.blockMap.block).filter(
key => post.blockMap.block[key]?.value?.parent_id === post.id
)
post.toc = getPageTableOfContents(post, post.blockMap)
}
}
@@ -65,8 +67,7 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
const paths = allPages?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug } }))
const paths = allPages?.filter(row => checkSlug(row)).map(row => ({ params: { prefix: row.slug } }))
return {
paths: paths,
fallback: true
@@ -83,8 +84,8 @@ export async function getStaticProps({ params: { prefix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -117,7 +118,7 @@ export async function getStaticProps({ params: { prefix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null
@@ -171,7 +172,7 @@ function checkSlug(row) {
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return ((slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug)) && row.type.indexOf('Menu') < 0
return (slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug) && row.type.indexOf('Menu') < 0
}
export default Slug

View File

@@ -1,9 +1,8 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import React from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 分类页
@@ -28,10 +27,10 @@ export async function getStaticProps({ params: { category } }) {
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
delete props.allPages

View File

@@ -1,9 +1,8 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import React from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 分类页
@@ -23,11 +22,13 @@ export async function getStaticProps({ params: { category, page } }) {
let props = await getGlobalData({ from })
// 过滤状态类型
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post.category && post.category.includes(category))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
delete props.allPages
props.page = page
@@ -47,10 +48,12 @@ export async function getStaticPaths() {
categoryOptions?.forEach(category => {
// 过滤状态类型
const categoryPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category.name))
const categoryPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post.category && post.category.includes(category.name))
// 处理文章页数
const postCount = categoryPosts.length
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { category: category.name, page: '' + i } })

View File

@@ -1,9 +1,9 @@
import BLOG from '@/blog.config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { generateRss } from '@/lib/rss'
import { generateRobotsTxt } from '@/lib/robots.txt'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { generateRobotsTxt } from '@/lib/robots.txt'
import { generateRss } from '@/lib/rss'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -28,20 +28,20 @@ export async function getStaticProps() {
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表默认给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
// 预览文章内容
if (BLOG.POST_LIST_PREVIEW === 'true') {
if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
}
}

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 文章列表分页
@@ -19,7 +19,7 @@ const Page = props => {
export async function getStaticPaths() {
const from = 'page-paths'
const { postCount } = await getGlobalData({ from })
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
return {
// remove first page, we 're not gonna handle that.
paths: Array.from({ length: totalPages - 1 }, (_, i) => ({
@@ -35,17 +35,17 @@ export async function getStaticProps({ params: { page } }) {
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
// 处理分页
props.posts = allPosts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = allPosts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.page = page
// 处理预览
if (BLOG.POST_LIST_PREVIEW === 'true') {
if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
}
}

View File

@@ -1,9 +1,9 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
// 根据页面路径加载不同Layout文件
@@ -27,10 +27,10 @@ export async function getStaticProps({ params: { keyword } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.keyword = keyword
return {
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
const isIterable = obj =>
obj != null && typeof obj[Symbol.iterator] === 'function'
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引

View File

@@ -1,9 +1,9 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
const { keyword } = props
@@ -29,7 +29,7 @@ export async function getStaticProps({ params: { keyword, page } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.keyword = keyword
props.page = page
delete props.allPages
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
const isIterable = obj =>
obj != null && typeof obj[Symbol.iterator] === 'function'
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引

View File

@@ -1,8 +1,8 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 标签下的文章列表
@@ -21,16 +21,18 @@ export async function getStaticProps({ params: { tag } }) {
const props = await getGlobalData({ from })
// 过滤状态
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.tag = tag

View File

@@ -1,8 +1,8 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Tag = props => {
// 根据页面路径加载不同Layout文件
@@ -14,11 +14,13 @@ export async function getStaticProps({ params: { tag, page } }) {
const from = 'tag-page-props'
const props = await getGlobalData({ from })
// 过滤状态、标签
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.tag = tag
props.page = page
@@ -35,10 +37,12 @@ export async function getStaticPaths() {
const paths = []
tagOptions?.forEach(tag => {
// 过滤状态类型
const tagPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name))
const tagPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag.name))
// 处理文章页数
const postCount = tagPosts.length
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { tag: tag.name, page: '' + i } })

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Screen iFrame</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
</style>
</head>
<body>
<!-- <div style="position: absolute;
right: 0px;
bottom: 0px;
background: white;">
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> -->
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script>
var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = myParam;
</script>
<script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</html>

32
public/js/fullscreen.js Normal file
View File

@@ -0,0 +1,32 @@
window.toggleFullScreen = toggleFullScreen
function toggleFullScreen() {
var iframe = document.getElementById('myIframe')
if (!document.fullscreenElement) {
if (iframe.requestFullscreen) {
iframe.requestFullscreen()
} else if (iframe.mozRequestFullScreen) {
/* Firefox */
iframe.mozRequestFullScreen()
} else if (iframe.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
iframe.webkitRequestFullscreen()
} else if (iframe.msRequestFullscreen) {
/* IE/Edge */
iframe.msRequestFullscreen()
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen()
}
}
}

View File

@@ -76,9 +76,13 @@ nav {
}
.shadow-card {
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
box-shadow:
rgba(0, 0, 0, 0.07) 0px 1px 2px,
rgba(0, 0, 0, 0.07) 0px 2px 4px,
rgba(0, 0, 0, 0.07) 0px 4px 8px,
rgba(0, 0, 0, 0.07) 0px 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px,
rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.gt-meta {
@@ -106,7 +110,6 @@ nav {
backdrop-filter: blur(10px);
}
.medium-zoom-overlay {
background: none !important;
/* background: rgba(0, 0, 0, 0.01) none repeat scroll 0% 0% !important; */
@@ -157,7 +160,7 @@ nav {
/* twikoo 评论区超链接样式 */
.tk-main a {
@apply text-blue-700
@apply text-blue-700;
}
/* twikoo 内置的 element-ui 加载样式 */
@@ -167,7 +170,7 @@ nav {
/* Webmention style */
.webmention-block {
background: rgba(0, 116, 222, .2);
background: rgba(0, 116, 222, 0.2);
padding: 1rem 2rem;
border-radius: 5px;
}
@@ -176,11 +179,11 @@ nav {
font-style: italic;
font-weight: 700;
font-size: 16px;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
.webmention-block-intro a {
color: #0000EE;
color: #0000ee;
text-decoration: underline;
}
@@ -197,14 +200,14 @@ nav {
.webmention-counts .count {
font-weight: bold;
margin-right: .2rem;
margin-right: 0.2rem;
}
/* .webmention-counts .counts > span {
margin-right: .8rem;
} */
.webmention-counts .counts>span:not(:last-child):after {
content: "";
.webmention-counts .counts > span:not(:last-child):after {
content: '';
}
a.avatar-wrapper {
@@ -221,7 +224,7 @@ a.avatar-wrapper {
.avatar {
border-radius: 50%;
margin: 0;
border: 3px solid rgba(0, 116, 222, .5);
border: 3px solid rgba(0, 116, 222, 0.5);
}
.replies {
@@ -235,7 +238,7 @@ a.avatar-wrapper {
position: relative;
padding: 0;
align-items: flex-start;
margin-top: .6rem;
margin-top: 0.6rem;
}
.reply p {
@@ -255,4 +258,9 @@ a.avatar-wrapper {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
}
.writing-vertical {
writing-mode: vertical-rl; /* 竖向排列从右向左 */
text-orientation: upright; /* 文字方向正常 */
}

View 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>
</>
)
}

View File

@@ -0,0 +1,27 @@
import dynamic from 'next/dynamic'
const NotionPage = dynamic(() => import('@/components/NotionPage'))
/**
* 公告
* @param {*} param0
* @returns
*/
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} />
</div>
)}
</section>
</div>
)
} else {
return null
}
}
export default Announcement

View 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'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View 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>{' '}
&nbsp;
<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>
)
}

View 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 <></>
}
}

View File

@@ -0,0 +1,25 @@
import { siteConfig } from '@/lib/config'
import { GameListIndexCombine } from './GameListIndexCombine'
import PaginationSimple from './PaginationSimple'
/**
* 分页博客列表
* @param {*} props
* @returns
*/
export const BlogListPage = props => {
const { page = 1, postCount } = props
const totalPage = Math.ceil(
postCount / parseInt(siteConfig('POSTS_PER_PAGE'))
)
const showNext = page < totalPage
return (
<>
<div id='posts-wrapper' className='my-4 select-none'>
<GameListIndexCombine {...props} />
</div>
<PaginationSimple page={page} showNext={showNext} />
</>
)
}

View File

@@ -0,0 +1,59 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { deepClone } from '@/lib/utils'
import throttle from 'lodash.throttle'
import { useCallback, useEffect, useRef, useState } from 'react'
import { GameListIndexCombine } from './GameListIndexCombine'
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='my-4' ref={targetRef}>
<GameListIndexCombine posts={postsToShow} />
</div>
<div onClick={handleGetMore} className='w-full my-4 py-4 text-center cursor-pointer '>
{' '}
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
</div>
</>
)
}

View 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

View File

@@ -0,0 +1,29 @@
import { useGlobal } from '@/lib/global'
/**
* 文章列表上方嵌入
* @param {*} props
* @returns
*/
export default function BlogPostBar(props) {
const { tag, category } = props
const { locale } = useGlobal()
if (tag) {
return (
<div className='flex items-center text-xl mt-4 px-2'>
<i className='mr-2 fas fa-tag' />
{locale.COMMON.TAGS}:{tag}
</div>
)
} else if (category) {
return (
<div className='flex items-center text-xl mt-4 px-2'>
<i className='mr-2 fas fa-th' />
{locale.COMMON.CATEGORY}:{category}
</div>
)
} else {
return <></>
}
}

View File

@@ -0,0 +1,33 @@
import { useGlobal } from '@/lib/global'
import { useImperativeHandle } from 'react'
/**
* 深色模式按钮
*/
const DarkModeButton = props => {
const { cRef, className } = props
const { isDarkMode, toggleDarkMode } = useGlobal()
/**
* 对外暴露方法
*/
useImperativeHandle(cRef, () => {
return {
handleChangeDarkMode: () => {
toggleDarkMode()
}
}
})
return (
<div
onClick={toggleDarkMode}
className={`${className || ''} flex items-center`}>
<i
className={`w-6 mr-2 fas ${isDarkMode ? 'fa-sun' : 'fa-moon px-0.5'}`}
/>
{isDarkMode ? 'Dark Mode' : 'Light Mode'}{' '}
</div>
)
}
export default DarkModeButton

View 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

View File

@@ -0,0 +1,33 @@
import { siteConfig } from '@/lib/config'
export const Footer = props => {
const d = new Date()
const currentYear = d.getFullYear()
const since = siteConfig('SINCE')
const copyrightDate =
parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
return (
<footer
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full dark:text-gray-200 `}>
<hr className='my-2 border-black dark:border-gray-100' />
{/* 页面底部 */}
<div className='w-full flex justify-between p-4 '>
<p>
© {siteConfig('TITLE')} {copyrightDate}
</p>
<p>{siteConfig('DESCRIPTION')}</p>
<span className='dark:text-gray-200 no-underline ml-4'>
Powered by
<a
href='https://github.com/tangly1024/NotionNext'
className=' hover:underline'>
{' '}
NotionNext {siteConfig('VERSION')}{' '}
</a>
</span>
</div>
</footer>
)
}

View File

@@ -0,0 +1,31 @@
/* eslint-disable @next/next/no-img-element */
/**
* 全屏按钮
* @returns
*/
export default function FullScreen() {
function toggleFullScreen() {
// window.scrollTo(0, 2)
document?.querySelector('#game-wrapper')?.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen &&
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}>
<i
alt='full screen'
title='full screen'
className='cursor-pointer fas fa-expand group-hover:scale-125 transition-all duration-150 '
/>
<span className='h-full flex mx-2 md:hidden items-center select-none'>FullScreen</span>
</div>
)
}

View File

@@ -0,0 +1,166 @@
/* eslint-disable @next/next/no-img-element */
import { AdSlot } from '@/components/GoogleAdsense'
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
import CONFIG from '../config'
/**
* 游戏列表
* @returns
*/
export const GameListIndexCombine = ({ posts }) => {
const gamesClone = deepClone(posts)
// 构造一个List<Component>
const components = []
// 根据序号随机大小;或根据game.recommend 决定
const recommend = siteConfig('GAME_INDEX_EXPAND_RECOMMEND', true, CONFIG)
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.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG))) {
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
continue
} else {
groupItems.push(item)
}
}
if (groupItems.length === 4) {
components.push(<GameItemGroup key={index} items={groupItems} />)
// 清空4合一卡片
groupItems = []
} else {
// 剩余的4合一不满4个的给他放大卡
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 { title } = item
const img = item.pageCoverThumbnail
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const video = item?.ext?.video
return (
<Link
href={`${url}`}
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}
/>
</Link>
)
}

View File

@@ -0,0 +1,74 @@
/* eslint-disable @next/next/no-img-element */
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
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 { title } = item
const img = item.pageCoverThumbnail
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const video = item?.ext?.video
return (
<Link
href={`${url}`}
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} />
</Link>
)
}

View File

@@ -0,0 +1,82 @@
/* eslint-disable @next/next/no-img-element */
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
/**
* 游戏列表- 关联游戏,在详情页展示
* @returns
*/
export const GameListRelate = ({ posts }) => {
const gamesClone = deepClone(posts)
// 构造一个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 justify-start gap-2'>
{components?.map((ItemComponent, index) => {
return ItemComponent
})}
</div>
</div>
)
}
/**
* 游戏=单卡
* @param {*} param0
* @returns
*/
const GameItem = ({ item }) => {
const { title } = item
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
return (
<Link
href={`${url}`}
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}
/>
</Link>
)
}

View File

@@ -0,0 +1,89 @@
/* eslint-disable @next/next/no-img-element */
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import { useState } from 'react'
import { useGameGlobal } from '..'
/**
* 游戏列表- 最近游戏
* @returns
*/
export const GameListRecent = ({ maxCount = 14 }) => {
const { recentGames } = useGameGlobal()
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='game-list-recent-wrapper w-full max-w-full overflow-x-auto pt-4 px-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 { title } = item || {}
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
return (
<a
href={`${url}`}
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 right-0.5 top-1 z-20'>
<i className='fas fa-clock-rotate-left w-6 h-6 flex items-center justify-center shadow rounded-full bg-white text-blue-500 text-sm' />
</div>
<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>
)
}

View File

@@ -0,0 +1,41 @@
import Link from 'next/link'
function GroupCategory({ currentCategory, categoryOptions }) {
if (!categoryOptions) {
return <></>
}
return (
<>
<Link className='mx-2' href='/category'>
<i className='fas fa-bars' />
</Link>
<div id='category-list' className='dark:border-gray-600 flex py-1'>
{categoryOptions.map(category => {
const selected = currentCategory === category.name
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
className={` ${
selected
? 'bg-green-500 text-white '
: 'dark:text-gray-300 hover:bg-green-500 rounded-lg hover:text-white'
} whitespace-nowrap overflow-ellipsis w-full items-center px-2 cursor-pointer py-1 font-bold`}>
{/* <i
className={`${selected ? 'text-white fa-folder-open' : 'fa-folder text-gray-400'} fas mr-2`}
/> */}
{category.name}
{/* <span className='text-xs flex items-start pl-2 h-full'>
{category.count}
</span> */}
</Link>
)
})}
</div>
</>
)
}
export default GroupCategory

View File

@@ -0,0 +1,28 @@
import Link from 'next/link'
import TagItemMini from './TagItemMini'
/**
* 标签组
* @param tags
* @param currentTag
* @returns {JSX.Element}
* @constructor
*/
function GroupTag({ tagOptions, currentTag }) {
if (!tagOptions) return <></>
return (
<>
<Link href='/tag'>
<i className='fas fa-tags p-2' />
</Link>
<div id='tags-group' className='flex flex-wrap p-1 gap-2'>
{tagOptions?.slice(0, 20)?.map(tag => {
const selected = tag.name === currentTag
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
})}
</div>
</>
)
}
export default GroupTag

View File

@@ -0,0 +1,25 @@
import { useGameGlobal } from '..'
import Logo from './Logo'
/**
* 顶栏
* @returns
*/
export default function Header() {
const { setSideBarVisible } = useGameGlobal()
return (
<header className='z-20'>
<div className='w-full h-16 rounded-md bg-white shadow-md hover:shadow-xl transition-shadow duration-200 dark:bg-[#1F2030] flex justify-between items-center px-4'>
<Logo />
<button
className='flex xl:hidden'
onClick={() => {
setSideBarVisible(true)
}}>
<i className='fas fa-search' />
</button>
</div>
</header>
)
}

View 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

View 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 dark:text-white font-bold font-serif'>{siteConfig('TITLE')}</h1>
<h2 className='text-xs text-gray-400 whitespace-nowrap'>{siteConfig('BIO')}</h2>
</div>
</Link>
)
}

View 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>
)
}

View 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>}
</>
}

View File

@@ -0,0 +1,68 @@
import Link from 'next/link'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
if (!link || !link.show) {
return null
}
const hasSubMenu = link?.subMenus?.length > 0
return (
<li>
<div
className='cursor-pointer relative'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<div className='dark:text-gray-50 nav hover:scale-105 transition-transform duration-200'>
<Link
href={link?.to}
className='flex flex-nowrap'
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div className='w-6 mr-2 text-center'>
{link?.icon && <i className={link?.icon} />}
</div>
{link?.name}
</Link>
</div>
)}
{hasSubMenu && (
<div className='dark:text-gray-50 nav'>
{link?.icon && <i className={`${link?.icon} w-6 text-center`} />}{' '}
{link?.name}
<i
className={`absolute right-0 top-0 px-2 h-full flex items-center fas fa-chevron-left duration-500 transition-all ${show ? ' rotate-180' : ''} `}></i>
</div>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 -left-5 ml-40' : 'invisible opacity-0 -left-4 '} rounded shadow-md z-30 -mt-2 py-2 px-4 absolute top-0 hover:scale-105 transition-all duration-200 border-gray-100 bg-white dark:bg-black`}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='text-gray-700 dark:text-gray-200 tracking-widest transition-all duration-200 '>
<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}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
)
})}
</ul>
)}
</div>
</li>
)
}

View File

@@ -0,0 +1,76 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { useGameGlobal } from '..'
import CONFIG from '../config'
import DarkModeButton from './DarkModeButton'
import { MenuItemDrop } from './MenuItemDrop'
/**
* 导航菜单
*/
export const MenuList = props => {
const { setSideBarVisible } = useGameGlobal()
const { customNav, customMenu } = props
const { locale } = useGlobal()
const defaultLinks = [
{
id: 1,
icon: 'fas fa-home',
name: locale.NAV.INDEX,
to: '/' || '/',
show: true
},
{
id: 2,
icon: 'fas fa-th',
name: locale.COMMON.CATEGORY,
to: '/category',
show: siteConfig('GAME_MENU_CATEGORY', null, CONFIG)
},
{
id: 3,
icon: 'fas fa-tag',
name: locale.COMMON.TAGS,
to: '/tag',
show: siteConfig('GAME_MENU_TAG', null, CONFIG)
}
]
let links = [].concat(defaultLinks)
if (customNav) {
links = defaultLinks.concat(customNav)
}
// 如果 开启自定义菜单则覆盖Page生成的菜单
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
return (
<ul
className={`dark:text-white p-4 space-y-4 shadow-md hover:shadow-xl transition-shadow duration-200 bg-white dark:bg-hexo-black-gray my-4 rounded-md`}>
<li>
<button
className='flex items-center hover:scale-105 transition-transform duration-200'
onClick={() => {
setSideBarVisible(true)
}}>
<i className='fas fa-search w-6 mr-2' />
<span>Search</span>
</button>
</li>
<li>
<button className='flex items-center hover:scale-105 transition-transform duration-200'>
{/* 切换深色模式 */}
<DarkModeButton className='text-center' />
</button>
</li>
{links?.length > 0 && <hr />}
{links?.map(
(link, index) =>
link && link.show && <MenuItemDrop key={index} link={link} />
)}
</ul>
)
}

View File

@@ -0,0 +1,54 @@
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { useRouter } from 'next/router'
/**
* 简易翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const PaginationSimple = ({ page, showNext }) => {
const { locale } = useGlobal()
const router = useRouter()
const currentPage = +page
const pagePrefix = router.asPath
.split('?')[0]
.replace(/\/page\/[1-9]\d*/, '')
.replace(/\/$/, '')
return (
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
<Link
href={{
pathname:
currentPage === 2
? `${pagePrefix}/`
: `${pagePrefix}/page/${currentPage - 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
passHref
rel='prev'
className={`${
currentPage === 1 ? 'invisible' : 'visible'
} text-center w-full duration-200 px-4 py-2 hover:border-black dark:border-hexo-black-gray border-b-2 hover:font-bold`}>
{locale.PAGINATION.PREV}
</Link>
<Link
href={{
pathname: `${pagePrefix}/page/${currentPage + 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
passHref
rel='next'
className={`${
showNext ? 'visible' : 'invisible'
} text-center w-full duration-200 px-4 py-2 hover:border-black dark:border-hexo-black-gray border-b-2 hover:font-bold`}>
{locale.PAGINATION.NEXT}
</Link>
</div>
)
}
export default PaginationSimple

View File

@@ -0,0 +1,53 @@
import NotionIcon from '@/components/NotionIcon'
import Link from 'next/link'
import TagItem from './TagItem'
/**
* 文章详情页说明信息
*/
export default function PostInfo(props) {
const { post } = props
return (
<section className='flex-wrap flex mt-2 text-gray--600 dark:text-gray-400 font-light leading-8'>
<div>
<div>
{post?.type !== 'Page' && (
<>
<Link
href={`/category/${post?.category}`}
passHref
className='cursor-pointer text-xs font-bold hover:underline mr-2'>
{post?.category}
</Link>
</>
)}
</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 my-2 items-start text-gray-500 dark:text-gray-400'>
{post?.tags && (
<div className='flex flex-wrap 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-fire' />
&nbsp;
<span className='mr-2 busuanzi_value_page_pv' />
</span>
</nav>
</>
)}
</div>
</section>
)
}

View 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>
)
}

View 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>
</>
)
}

View 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

View 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>
);
}

View File

@@ -0,0 +1,65 @@
import { siteConfig } from '@/lib/config'
import { deepClone } from '@/lib/utils'
import { useEffect, useRef } from 'react'
import { useGameGlobal } from '..'
import CONFIG from '../config'
import { GameListNormal } from './GameListNormal'
import Logo from './Logo'
/**
* 侧拉抽屉的内容
*/
export default function SideBarContent() {
const { allNavPages, sideBarVisible, setSideBarVisible, filterGames, setFilterGames } = useGameGlobal()
const inputRef = useRef(null) // 创建对输入框的引用
const allGames = deepClone(allNavPages)
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.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)))
)
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>
)
}

View File

@@ -0,0 +1,61 @@
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-[#83FFE7] dark: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

View 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>
}

View File

@@ -0,0 +1,11 @@
import Link from 'next/link'
const TagItem = ({ tag }) => (
<Link href={`/tag/${encodeURIComponent(tag)}`}>
<p className='cursor-pointer hover:bg-gray-50 dark:hover:bg-hexo-black-gray mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600'>
{tag}
</p>
</Link>
)
export default TagItem

View File

@@ -0,0 +1,21 @@
import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => {
return (
<Link
key={tag}
href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`}
className={` rounded hover:text-white hover:bg-green-500 text-black dark:text-white dark:bg-gray-800 py-0.5 px-1 `}
passHref>
{/* # {tag.name} */}
<span className='flex flex-nowrap cursor-pointer'>
# <span>{tag.name}</span>{' '}
<span className='h-full flex items-start text-xs ml-1'>
{tag.count ? `${tag.count}` : ''}
</span>
</span>
</Link>
)
}
export default TagItemMini

View 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

View 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>
}

18
themes/game/config.js Normal file
View File

@@ -0,0 +1,18 @@
const CONFIG = {
GAME_NAV_NOTION_ICON: true, // 是否读取Notion图标作为站点头像 ; 否则默认显示黑色SVG方块
GAME_RECOMMEND_TAG: 'Recommend', // 打了此Tag被视为推荐
GAME_INDEX_EXPAND_RECOMMEND: true, // 首页列表是否将推荐游戏放大,否则随机放大。
// 特殊菜单
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

523
themes/game/index.js Normal file
View File

@@ -0,0 +1,523 @@
import Comment from '@/components/Comment'
import { Draggable } from '@/components/Draggable'
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, shuffleArray } from '@/lib/utils'
import Link from 'next/link'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import Announcement from './components/Announcement'
import { ArticleLock } from './components/ArticleLock'
import BlogArchiveItem from './components/BlogArchiveItem'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import BlogPostBar from './components/BlogPostBar'
import { Footer } from './components/Footer'
import FullScreen from './components/FullScreen'
import { GameListIndexCombine } from './components/GameListIndexCombine'
import { GameListRelate } from './components/GameListRealate'
import { GameListRecent } from './components/GameListRecent'
import GroupCategory from './components/GroupCategory'
import GroupTag from './components/GroupTag'
import Header from './components/Header'
import { MenuList } from './components/MenuList'
import PostInfo from './components/PostInfo'
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.tags?.some(
t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)
)
)
)
)
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 bg-[#83FFE7] dark:bg-black dark:text-gray-300 scroll-smooth`}>
<Style />
{/* 左右布局 */}
<div
id='wrapper'
className={'relative flex justify-between w-full h-full mx-auto'}>
{/* PC端左侧 */}
<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='select-none'>
{/* 抬头logo等 */}
<Header />
{/* 菜单栏 */}
<MenuList {...props} />
</div>
{/* 左侧广告栏目 */}
<div className='w-full'>
<AdSlot />
</div>
</div>
</div>
{/* 右侧 */}
<main className='flex-grow w-full h-full flex flex-col min-h-screen overflow-x-auto'>
<div className='flex-grow h-full'>{children}</div>
<Footer />
</main>
</div>
<SideBarDrawer
isOpen={sideBarVisible}
onClose={() => {
setSideBarVisible(false)
}}>
<SideBarContent />
</SideBarDrawer>
</div>
</ThemeGlobalGame.Provider>
)
}
/**
* 首页
* 首页是个博客列表,加上顶部嵌入一个公告
* @param {*} props
* @returns
*/
const LayoutIndex = props => {
const { tagOptions, currentTag, categoryOptions, currentCategory } = props
return (
<>
{/* 首页移动端顶部导航 */}
<div className='p-2 xl:hidden'>
<Header />
</div>
{/* 最近游戏 */}
<GameListRecent />
{/* 游戏列表 */}
<LayoutPostList {...props} />
{/* 主区域下方 */}
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
{/* 标签汇总 */}
<GroupCategory
categoryOptions={categoryOptions}
currentCategory={currentCategory}
/>
<hr />
<GroupTag tagOptions={tagOptions} currentTag={currentTag} />
</div>
{/* 广告 */}
<div className='w-full'>
<AdSlot type='in-article' />
</div>
{/* 站点公告信息 */}
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
<Announcement {...props} />
</div>
</>
)
}
/**
* 博客列表
* @param {*} props
* @returns
*/
const LayoutPostList = props => {
const { posts } = 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 (
<>
<BlogPostBar {...props} />
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogListPage posts={filteredBlogPosts} {...props} />
) : (
<BlogListScroll posts={filteredBlogPosts} {...props} />
)}
</>
)
}
/**
* 搜索
* 页面是博客列表,上方嵌入一个搜索引导条
* @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 (
<>
{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, allNavPages, recommendPosts, lock, validPassword } = props
const game = deepClone(post)
const [loading, setLoading] = useState(false)
// const [url, setUrl] = useState(game?.ext?.href)
const relateGames = recommendPosts
const randomGames = shuffleArray(deepClone(allNavPages))
// 将当前游戏加入到最近游玩
useEffect(() => {
// 更新最新游戏
const recentGames = localStorage.getItem('recent_games')
? JSON.parse(localStorage.getItem('recent_games'))
: []
const existedIndex = recentGames.findIndex(item => item?.id === game?.id)
if (existedIndex === -1) {
recentGames.unshift(game) // 将游戏插入到数组头部
} else {
// 如果游戏已存在于数组中,将其移至数组头部
const existingGame = recentGames.splice(existedIndex, 1)[0]
recentGames.unshift(existingGame)
}
localStorage.setItem('recent_games', JSON.stringify(recentGames))
const iframe = document.getElementById('game-wrapper')
// 定义一个函数来处理iframe加载成功事件
function iframeLoaded() {
if (game) {
setLoading(false)
}
}
// 绑定加载事件
if (iframe?.attachEvent) {
iframe?.attachEvent('onload', iframeLoaded)
} else {
if (iframe) iframe.onload = iframeLoaded
}
// 更改iFrame的title
if (
document
?.getElementById('game-wrapper')
?.contentDocument.querySelector('title')?.textContent
) {
document
.getElementById('game-wrapper')
.contentDocument.querySelector('title').textContent = `${
game?.title || ''
} - Play ${game?.title || ''} on ${siteConfig('TITLE')}`
}
}, [game])
return (
<>
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && (
<div id='article-wrapper' className='md:px-2'>
{/* 游戏区域 */}
<div className='game-detail-wrapper w-full grow flex md:px-2'>
{/* 移动端返回主页按钮 */}
<Draggable stick='left'>
<div
style={{ left: '0px', top: '1rem' }}
className='fixed xl:hidden group space-x-1 flex items-center z-20 pr-3 pl-1 bg-[#202030] rounded-r-2xl shadow-lg '>
<Link
href='/'
className='px-1 py-3 hover:scale-125 duration-200 transition-all'
passHref>
<i className='fas fa-arrow-left' />
</Link>{' '}
<span
className='text-white font-serif'
onClick={() => {
document.querySelector('.game-info').scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}}>
G
</span>
</div>
</Draggable>
<div className='w-full py-1 md:py-4'>
{/* 游戏区 */}
<div className='bg-black w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md relative'>
{/* Loading遮罩 */}
{loading && (
<div className='absolute z-20 w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md overflow-hidden '>
<div className='z-20 absolute bg-black bg-opacity-75 w-full h-full flex flex-col gap-4 justify-center items-center'>
<h2 className='text-3xl text-white flex gap-2'>
<i className='fas fa-spinner animate-spin'></i>
{siteConfig('TITLE')}
</h2>
<h3 className='text-xl text-white'>
{siteConfig('DESCRIPTION')}
</h3>
</div>
{/* 游戏封面图 */}
{/* eslint-disable-next-line @next/next/no-img-element */}
{game?.img && (
<img
src={game?.img}
className='w-full h-full blur-md absolute top-0 left-0 z-0'
/>
)}
</div>
)}
<iframe
id='game-wrapper'
className={`w-full xl:h-[calc(100vh-8rem)] h-screen md:rounded-md overflow-hidden ${game?.ext?.href ? '' : 'hidden'}`}
style={{
position: 'relative'
}}
src={game?.ext?.href}></iframe>
{/* 游戏窗口装饰器 */}
{game && !loading && (
<div className='game-decorator bg-[#0B0D14] right-0 bottom-0 flex justify-center h-12 md:w-12 z-10 md:absolute'>
{/* 加入全屏按钮 */}
<FullScreen />
</div>
)}
</div>
{/* 游戏资讯 */}
<div className='game-info dark:text-white py-4 px-2 md:px-0 mt-8 md:mt-0'>
{/* 关联游戏 */}
<div className='w-full'>
<GameListRelate posts={relateGames} />
</div>
{game && (
<div className='bg-white shadow-md my-2 p-2 rounded-md dark:bg-black'>
<PostInfo post={post} />
<NotionPage post={post} />
{/* 广告嵌入 */}
<AdSlot />
<ShareBar post={post} />
<Comment frontMatter={post} />
</div>
)}
</div>
</div>
</div>
{/* 其它游戏列表 */}
<GameListIndexCombine posts={randomGames} />
</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 my-4 gap-2'>
{categoryOptions?.map(category => {
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<div
className={
'bg-white rounded-lg 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 my-4 gap-2'>
{tagOptions.map(tag => {
return (
<Link
key={tag.name}
href={`/tag/${encodeURIComponent(tag.name)}`}
passHref
className={` select-none cursor-pointer flex bg-white rounded-lg hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white hover:shadow-xl dark:bg-gray-800`}>
<i className='mr-1 fas fa-tag' />{' '}
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
</Link>
)
})}
</div>
</div>
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
}

18
themes/game/style.js Normal file
View 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 }

View File

@@ -10,9 +10,19 @@ export default function BlogPostBar(props) {
const { locale } = useGlobal()
if (tag) {
return <div className='flex items-center py-8'><div className='text-xl'><i className='mr-2 fas fa-tag' />{locale.COMMON.TAGS}:</div>{tag}</div>
return (
<div className='flex items-center text-xl py-8'>
<i className='mr-2 fas fa-tag' />
{locale.COMMON.TAGS}:{tag}
</div>
)
} else if (category) {
<div className='flex items-center py-8'><div className='text-xl'><i className='mr-2 fas fa-th' />{locale.COMMON.CATEGORY}:</div>{category}</div>
return (
<div className='flex items-center text-xl py-8'>
<i className='mr-2 fas fa-th' />
{locale.COMMON.CATEGORY}:{category}
</div>
)
} else {
return <></>
}

View File

@@ -0,0 +1,29 @@
import { useGlobal } from '@/lib/global'
/**
* 文章列表上方嵌入
* @param {*} props
* @returns
*/
export default function BlogPostBar(props) {
const { tag, category } = props
const { locale } = useGlobal()
if (tag) {
return (
<div className='flex items-center text-xl py-2'>
<i className='mr-2 fas fa-tag' />
{locale.COMMON.TAGS}: {tag}
</div>
)
} else if (category) {
return (
<div className='flex items-center text-xl py-2'>
<i className='mr-2 fas fa-th' />
{locale.COMMON.CATEGORY}: {category}
</div>
)
} else {
return <></>
}
}

View File

@@ -1,37 +1,59 @@
import CONFIG from './config'
import { createContext, useContext, useEffect, useRef } from 'react'
import { isBrowser } from '@/lib/utils'
import { useGlobal } from '@/lib/global'
import { AdSlot } from '@/components/GoogleAdsense'
import { siteConfig } from '@/lib/config'
import { Transition } from '@headlessui/react'
import Link from 'next/link'
import { Style } from './style'
import replaceSearchResult from '@/components/Mark'
import dynamic from 'next/dynamic'
import NotionPage from '@/components/NotionPage'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef } from 'react'
import BlogPostBar from './components/BlogPostBar'
import CONFIG from './config'
import { Style } from './style'
const AlgoliaSearchModal = dynamic(() => import('@/components/AlgoliaSearchModal'), { ssr: false })
const AlgoliaSearchModal = dynamic(
() => import('@/components/AlgoliaSearchModal'),
{ ssr: false }
)
// 主题组件
const BlogListScroll = dynamic(() => import('./components/BlogListScroll'), { ssr: false });
const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), { ssr: false });
const ArticleLock = dynamic(() => import('./components/ArticleLock'), { ssr: false });
const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), { ssr: false });
const Comment = dynamic(() => import('@/components/Comment'), { ssr: false });
const ArticleAround = dynamic(() => import('./components/ArticleAround'), { ssr: false });
const ShareBar = dynamic(() => import('@/components/ShareBar'), { ssr: false });
const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false });
const Header = dynamic(() => import('./components/Header'), { ssr: false });
const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false });
const SideBar = dynamic(() => import('./components/SideBar'), { ssr: false });
const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), { ssr: false });
const Footer = dynamic(() => import('./components/Footer'), { ssr: false });
const SearchInput = dynamic(() => import('./components/SearchInput'), { ssr: false });
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false });
const BlogListPage = dynamic(() => import('./components/BlogListPage'), { ssr: false })
const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), { ssr: false })
const BlogListScroll = dynamic(() => import('./components/BlogListScroll'), {
ssr: false
})
const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), {
ssr: false
})
const ArticleLock = dynamic(() => import('./components/ArticleLock'), {
ssr: false
})
const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), {
ssr: false
})
const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
const ArticleAround = dynamic(() => import('./components/ArticleAround'), {
ssr: false
})
const ShareBar = dynamic(() => import('@/components/ShareBar'), { ssr: false })
const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false })
const Header = dynamic(() => import('./components/Header'), { ssr: false })
const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false })
const SideBar = dynamic(() => import('./components/SideBar'), { ssr: false })
const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), {
ssr: false
})
const Footer = dynamic(() => import('./components/Footer'), { ssr: false })
const SearchInput = dynamic(() => import('./components/SearchInput'), {
ssr: false
})
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
const BlogListPage = dynamic(() => import('./components/BlogListPage'), {
ssr: false
})
const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), {
ssr: false
})
// 主题全局状态
const ThemeGlobalSimple = createContext()
@@ -50,57 +72,63 @@ const LayoutBase = props => {
return (
<ThemeGlobalSimple.Provider value={{ searchModal }}>
<div id='theme-simple' className={`${siteConfig('FONT_STYLE')} min-h-screen flex flex-col dark:text-gray-300 bg-white dark:bg-black scroll-smooth`}>
<div
id='theme-simple'
className={`${siteConfig('FONT_STYLE')} min-h-screen flex flex-col dark:text-gray-300 bg-white dark:bg-black scroll-smooth`}>
<Style />
<Style/>
{siteConfig('SIMPLE_TOP_BAR', null, CONFIG) && <TopBar {...props} />}
{siteConfig('SIMPLE_TOP_BAR', null, CONFIG) && <TopBar {...props} />}
{/* 顶部LOGO */}
<Header {...props} />
{/* 顶部LOGO */}
<Header {...props} />
{/* 导航栏 */}
<NavBar {...props} />
{/* 导航栏 */}
<NavBar {...props} />
{/* 主体 */}
<div
id='container-wrapper'
className={
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
? 'flex-row-reverse'
: '') + ' w-full flex-1 flex items-start max-w-9/10 mx-auto pt-12'
}>
<div id='container-inner ' className='w-full flex-grow min-h-fit'>
<Transition
show={!onLoading}
appear={true}
enter='transition ease-in-out duration-700 transform order-first'
enterFrom='opacity-0 translate-y-16'
enterTo='opacity-100'
leave='transition ease-in-out duration-300 transform'
leaveFrom='opacity-100 translate-y-0'
leaveTo='opacity-0 -translate-y-16'
unmount={false}>
{slotTop}
{/* 主体 */}
<div id='container-wrapper' className={(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ? 'flex-row-reverse' : '') + ' w-full flex-1 flex items-start max-w-9/10 mx-auto pt-12'}>
<div id='container-inner ' className='w-full flex-grow min-h-fit'>
<Transition
show={!onLoading}
appear={true}
enter="transition ease-in-out duration-700 transform order-first"
enterFrom="opacity-0 translate-y-16"
enterTo="opacity-100"
leave="transition ease-in-out duration-300 transform"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 -translate-y-16"
unmount={false}
>
{slotTop}
{children}
</Transition>
<AdSlot type='native' />
</div>
{children}
</Transition>
<AdSlot type='native' />
</div>
{fullWidth
? null
: <div id='right-sidebar' className="hidden xl:block flex-none sticky top-8 w-96 border-l dark:border-gray-800 pl-12 border-gray-100">
{fullWidth ? null : (
<div
id='right-sidebar'
className='hidden xl:block flex-none sticky top-8 w-96 border-l dark:border-gray-800 pl-12 border-gray-100'>
<SideBar {...props} />
</div>}
</div>
<div className='fixed right-4 bottom-4 z-20'>
<JumpToTopButton />
</div>
{/* 搜索框 */}
<AlgoliaSearchModal cRef={searchModal} {...props}/>
<Footer {...props} />
)}
</div>
<div className='fixed right-4 bottom-4 z-20'>
<JumpToTopButton />
</div>
{/* 搜索框 */}
<AlgoliaSearchModal cRef={searchModal} {...props} />
<Footer {...props} />
</div>
</ThemeGlobalSimple.Provider>
)
}
@@ -121,9 +149,14 @@ const LayoutIndex = props => {
*/
const LayoutPostList = props => {
return (
<>
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</>
<>
<BlogPostBar {...props} />
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogListPage {...props} />
) : (
<BlogListScroll {...props} />
)}
</>
)
}
@@ -149,7 +182,9 @@ const LayoutSearch = props => {
}
}, [])
const slotTop = siteConfig('ALGOLIA_APP_ID') ? null : <SearchInput {...props} />
const slotTop = siteConfig('ALGOLIA_APP_ID') ? null : (
<SearchInput {...props} />
)
return <LayoutPostList {...props} slotTop={slotTop} />
}
@@ -162,11 +197,17 @@ const LayoutSearch = props => {
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>
</>
<>
<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>
</>
)
}
@@ -180,39 +221,39 @@ const LayoutSlug = props => {
const { fullWidth } = useGlobal()
return (
<>
<>
{lock && <ArticleLock validPassword={validPassword} />}
{lock && <ArticleLock validPassword={validPassword} />}
<div
id='article-wrapper'
className={`px-2 ${fullWidth ? '' : 'xl:max-w-4xl 2xl:max-w-6xl'}`}>
{/* 文章信息 */}
<ArticleInfo post={post} />
<div id="article-wrapper" className={`px-2 ${fullWidth ? '' : 'xl:max-w-4xl 2xl:max-w-6xl'}`}>
{/* 广告嵌入 */}
{/* <AdSlot type={'in-article'} /> */}
<WWAds orientation='horizontal' className='w-full' />
{/* 文章信息 */}
<ArticleInfo post={post} />
{/* Notion文章主体 */}
{!lock && <NotionPage post={post} />}
{/* 广告嵌入 */}
{/* <AdSlot type={'in-article'} /> */}
<WWAds orientation="horizontal" className="w-full" />
{/* 分享 */}
<ShareBar post={post} />
{/* Notion文章主体 */}
{!lock && <NotionPage post={post} />}
{/* 广告嵌入 */}
<AdSlot type={'in-article'} />
{/* 分享 */}
<ShareBar post={post} />
{post?.type === 'Post' && (
<>
<ArticleAround prev={prev} next={next} />
<RecommendPosts recommendPosts={recommendPosts} />
</>
)}
{/* 广告嵌入 */}
<AdSlot type={'in-article'} />
{post?.type === 'Post' && <>
<ArticleAround prev={prev} next={next} />
<RecommendPosts recommendPosts={recommendPosts}/>
</>}
{/* 评论区 */}
<Comment frontMatter={post} />
</div>
</>
{/* 评论区 */}
<Comment frontMatter={post} />
</div>
</>
)
}
@@ -221,27 +262,28 @@ const LayoutSlug = props => {
* @param {*} props
* @returns
*/
const Layout404 = (props) => {
const Layout404 = props => {
const { post } = 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)
})
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)
},
siteConfig('POST_WAITING_TIME_FOR_404') * 1000
)
}
}, [post])
return <>
404 Not found.
</>
return <>404 Not found.</>
}
/**
@@ -252,24 +294,27 @@ const Layout404 = (props) => {
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>
</>
<>
<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>
</>
)
}
@@ -278,38 +323,41 @@ const LayoutCategoryIndex = props => {
* @param {*} props
* @returns
*/
const LayoutTagIndex = (props) => {
const LayoutTagIndex = props => {
const { tagOptions } = props
return (
<>
<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 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>
</>
)
}
export {
CONFIG as THEME_CONFIG,
LayoutBase,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutTagIndex
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
}

View File

@@ -1,8 +1,8 @@
import BLOG, { LAYOUT_MAPPINGS } from '@/blog.config'
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
import dynamic from 'next/dynamic'
import getConfig from 'next/config'
import * as ThemeComponents from '@theme-components'
import getConfig from 'next/config'
import dynamic from 'next/dynamic'
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
// 在next.config.js中扫描所有主题
export const { THEMES = [] } = getConfig().publicRuntimeConfig
@@ -100,7 +100,6 @@ export const initDarkMode = (updateDarkMode, defaultDarkMode) => {
// 查看localStorage中用户记录的是否深色模式
const userDarkMode = loadDarkModeFromLocalStorage()
console.log('深色模式',userDarkMode)
if (userDarkMode) {
newDarkMode = userDarkMode === 'dark' || userDarkMode === 'true'
}