Merge pull request #34 from tangly1024/theme-Next

Theme next
This commit is contained in:
tangly1024
2022-01-19 15:37:18 +08:00
committed by GitHub
119 changed files with 1692 additions and 731 deletions

View File

@@ -1,102 +1,68 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/zh/features/personality
const BLOG = {
title: '小唐笔记', // 站点标题
description: '分享编程技术与记录生活', // 站点描述
author: 'tangly1024', // 作者
bio: '一个普通的干饭人🍚', // 作者简介
email: 'tlyong1992@hotmail.com', // 联系邮箱
link: 'https://tangly1024.com', // 网站地址
keywords: ['Notion', '写作', '博客'], // 网站关键词
home: { // 首页
showHomeBanner: false, // 首页是否显示大图及标语 [true,false]
homeBannerStrings: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
homeBannerImage: './bg_image.jpg', // 背景图地址
showPostCover: false, // 文章列表显示封面图
showPreview: true, // 列表展示文章预览
previewLines: 12, // 预览文章的篇幅
showSummary: false // 显示用户自定义摘要
},
lang: 'zh-CN', // ['zh-CN','en-US'] default lang => see /lib/lang.js for more.
notionPageId: process.env.NOTION_PAGE_ID || 'bee1fccfa3bd47a1a7be83cc71372d83', // Important page_id
notionAccessToken: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
appearance: 'auto', // ['light', 'dark', 'auto'],
font: 'font-serif tracking-wider subpixel-antialiased', // 文章字体 ['font-sans', 'font-serif', 'font-mono'] @see https://www.tailwindcss.cn/docs/font-family
lightBackground: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
darkBackground: '#111827', // use hex value, don't forget '#'
path: '', // leave this empty unless you want to deploy in a folder
since: 2020, // if leave this empty, current year will be used.
postListStyle: 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
postsPerPage: 6, // post counts per page
sortByDate: false,
topNavType: 'normal', // ['fixed','autoCollapse','normal'] 分别是固定顶部、固定底部滑动时自动折叠,不固定
menu: { // 菜单栏设置
showAbout: false, // 显示关于
showCategory: true, // 显示分类
showTag: true, // 显示标签
showArchive: true, // 显示归档
showSearch: true // 显示搜索
},
widget: { // 挂件及组件设置
showPet: false, // 是否显示宠物挂件
petLink: 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
showToTop: true, // 是否显示回顶
showToBottom: false, // 显示回底
showDarkMode: false, // 显示日间/夜间模式切换
showToc: true, // 移动端显示悬浮目录
showShareBar: false, // 文章分享功能
showRelatePosts: true, // 相关文章推荐
showCopyRight: true, // 文章版权声明
showLatestPost: false, // 右侧边栏显示最近更新
showCategoryList: false, // 右侧边栏显示文章分类列表
showTagList: false // 右侧边栏显示标签分类列表
},
socialLink: { // 社交链接,如不需要展示可以留空白,例如 weibo:''
weibo: 'https://weibo.com/tangly1024',
twitter: 'https://twitter.com/troy1024_1',
github: 'https://github.com/tangly1024',
telegram: 'https://t.me/tangly_1024'
},
comment: { // 评论插件,支持 gitalk, utterances, cusdis
provider: 'gitalk', // 不需要则留空白
gitalkConfig: {
repo: 'NotionNext', // The repository of store comments
owner: 'tangly1024',
admin: ['tangly1024'],
clientID: process.env.GITALK_ID || 'be7864a16b693e256f8f',
clientSecret: process.env.GITALK_SECRET || 'dbd0f6d9ceea8940f6ed20936b415274b8fe66a2',
distractionFreeMode: false
},
cusdisConfig: {
appId: '445ba48e-f751-487f-b22f-cdbe3310d28f', // data-app-id
host: 'https://cusdis.com', // data-host, change this if you're using self-hosted version
scriptSrc: 'https://cusdis.com/js/cusdis.es.js' // change this if you're using self-hosted version
},
utterancesConfig: {
repo: 'tangly1024/NotionNext'
},
gitter: '', // gitter聊天室
DaoVoiceId: '', // DaoVoice http://dashboard.daovoice.io/get-started
TidioId: '' // https://www.tidio.com/
},
// --- 高级设置
analytics: { // 文章访问量统计
busuanzi: true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
provider: 'ga', // 支持 Google Analytics and Ackee, please fill with 'ga' or 'ackee', leave it empty to disable it.
baiduAnalytics: 'f683ef76f06bb187cbed5546f6f28f28', // e.g only need xxxxx -> https://hm.baidu.com/hm.js?[xxxxx]
cnzzAnalytics: '', // 站长统计id only need xxxxxxxx -> https://s9.cnzz.com/z_stat.php?id=[xxxxxxxx]&web_id=[xxxxxxx]
gaConfig: {
measurementId: 'G-68EK0W049N' // e.g: G-XXXXXXXXXX
},
ackeeConfig: {
tracker: '', // e.g 'https://ackee.tangly1024.net/tracker.js'
dataAckeeServer: '', // e.g https://ackee.tangly1024.net , don't end with a slash
domainId: '' // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
}
},
seo: {
googleSiteVerification: '' // Remove the value or replace it with your own google site verification code
},
googleAdsenseId: 'ca-pub-2708419466378217', // 谷歌广告ID
isProd: process.env.VERCEL_ENV === 'production'// distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题
DESCRIPTION: process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述
AUTHOR: 'tangly1024', // 作者
BIO: '一个普通的干饭人🍚', // 作者简介
LINK: 'https://tangly1024.com', // 网站地址
KEYWORDS: 'Notion, 博客', // 网站关键词 英文逗号隔开
NOTION_PAGE_ID: process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5', // Important page_idDuplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
LANG: 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
SINCE: 2021, // e.g if leave this empty, current year will be used.
BEI_AN: '', // 备案号 闽ICP备XXXXXXX
APPEARANCE: 'auto', // ['light', 'dark', 'auto'],
FONT: 'font-serif tracking-wider subpixel-antialiased', // 文章字体 ['font-sans', 'font-serif', 'font-mono'] @see https://www.tailwindcss.cn/docs/font-family
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#111827', // use hex value, don't forget '#'
PATH: '', // leave this empty unless you want to deploy in a folder
POST_LIST_STYLE: 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
POST_PREVIEW_LINES: 12, // 预览博客行数
POSTS_PER_PAGE: 6, // post counts per page
POSTS_SORT_BY: 'notion', // 排序方式 'date'按时间,'notion'由notion控制
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
CONTACT_EMAIL: 'tlyong1992@hotmail.com',
CONTACT_WEIBO: 'https://weibo.com/tangly1024',
CONTACT_TWITTER: 'https://twitter.com/troy1024_1',
CONTACT_GITHUB: 'https://github.com/tangly1024',
CONTACT_TELEGRAM: 'https://t.me/tangly_1024',
// 评论互动 可同时开启 CUSDIS UTTERRANCES GITALK
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 || 'https://cusdis.com/js/cusdis.es.js', // change this if you're using self-hosted version
COMMENT_UTTERRANCES_REPO: process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // e.g 'tangly1024/NotionNext' see https://utteranc.es/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // e.g NotionNext see https://gitalk.github.io/
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
COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID
COMMENT_GITALK_DISTRACTION_FREE_MODE: false,
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
COMMENT_TIDIO_ID: process.env.NEXT_PUBLIC_COMMENT_TIDIO_ID || '', // [tidio_id] -> //code.tidio.co/[tidio_id].js
// 站点统计
ANALYTICS_BUSUANZI_ENABLE: true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
ANALYTICS_BAIDU_ID: process.env.NEXT_PUBLIC_ANALYTICS_BAIDU_ID || '', // e.g 只需要填写百度统计的id[baidu_id] -> https://hm.baidu.com/hm.js?[baidu_id]
ANALYTICS_CNZZ_ID: process.env.NEXT_PUBLIC_ANALYTICS_CNZZ_ID || '', // 只需要填写站长统计的id, [cnzz_id] -> https://s9.cnzz.com/z_stat.php?id=[cnzz_id]&web_id=[cnzz_id]
ANALYTICS_GOOGLE_ID: process.env.NEXT_PUBLIC_ANALYTICS_GOOGLE_ID || '', // 谷歌Analytics的id e.g: G-XXXXXXXXXX
ANALYTICS_ACKEE_TRACKER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.net/tracker.js'
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.net , don't end with a slash
ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
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
ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
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)
}
// export default BLOG
module.exports = BLOG

View File

@@ -1,12 +1,19 @@
import { useRouter } from 'next/router'
import useAckee from 'use-ackee'
import BLOG from '@/blog.config'
const Ackee = ({ ackeeServerUrl, ackeeDomainId }) => {
const Ackee = () => {
const router = useRouter()
useAckee(
router.asPath,
{ server: ackeeServerUrl, domainId: ackeeDomainId },
{ detailed: false, ignoreLocalhost: true }
{
server: BLOG.ANALYTICS_ACKEE_DATA_SERVER,
domainId: BLOG.ANALYTICS_ACKEE_DOMAIN_ID
},
{
detailed: false,
ignoreLocalhost: true
}
)
return null
}

View File

@@ -3,6 +3,7 @@ import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import 'gitalk/dist/gitalk.css'
import Tabs from '@/components/Tabs'
const GitalkComponent = dynamic(
() => {
@@ -26,46 +27,42 @@ const CusdisComponent = dynamic(
const Comment = ({ frontMatter }) => {
const router = useRouter()
const { theme } = useGlobal()
return (
BLOG.comment.provider !== '' && (
<div className='comment mt-5 text-gray-800 dark:text-gray-300'>
{BLOG.comment.provider === 'gitalk' && (<div className='m-10'>
<div className='comment mt-5 px-10 text-gray-800 dark:text-gray-300'>
<Tabs>
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
<CusdisComponent
attrs={{
host: BLOG.COMMENT_CUSDIS_HOST,
appId: BLOG.COMMENT_CUSDIS_APP_ID,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.LINK + router.asPath,
theme: theme
}}
lang={BLOG.LANG.toLowerCase()}
/>
</div>)}
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
<GitalkComponent
options={{
id: frontMatter.id,
title: frontMatter.title,
clientID: BLOG.comment.gitalkConfig.clientID,
clientSecret: BLOG.comment.gitalkConfig.clientSecret,
repo: BLOG.comment.gitalkConfig.repo,
owner: BLOG.comment.gitalkConfig.owner,
admin: BLOG.comment.gitalkConfig.admin,
distractionFreeMode: BLOG.comment.gitalkConfig.distractionFreeMode
clientID: BLOG.COMMENT_GITALK_CLIENT_ID,
clientSecret: BLOG.COMMENT_GITALK_CLIENT_SECRET,
repo: BLOG.COMMENT_GITALK_REPO,
owner: BLOG.COMMENT_GITALK_OWNER,
admin: BLOG.COMMENT_GITALK_ADMIN.split(','),
distractionFreeMode: JSON.parse(BLOG.COMMENT_GITALK_DISTRACTION_FREE_MODE)
}}
/>
</div>)}
{BLOG.comment.provider === 'utterances' && (<div className='m-10'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
)}
{BLOG.comment.provider === 'cusdis' && (<>
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
<div className='m-10'>
<CusdisComponent
attrs={{
host: BLOG.comment.cusdisConfig.host,
appId: BLOG.comment.cusdisConfig.appId,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.link + router.asPath,
theme: theme
}}
lang={BLOG.lang.toLowerCase()}
/>
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
</>)}
</div>
)
)}
</Tabs>
</div>
)
}

View File

@@ -2,32 +2,27 @@ import BLOG from '@/blog.config'
import Head from 'next/head'
const CommonHead = ({ meta }) => {
let url = BLOG.path.length ? `${BLOG.link}/${BLOG.path}` : BLOG.link
let url = BLOG.PATH.length ? `${BLOG.LINK}/${BLOG.PATH}` : BLOG.LINK
if (meta) {
url = `${url}/${meta.slug}`
}
const title = meta?.title || BLOG.title
const description = meta?.description || BLOG.description
const title = meta?.title || BLOG.TITLE
const description = meta?.description || BLOG.DESCRIPTION
const type = meta?.type || 'website'
const keywords = meta?.tags || BLOG.keywords
const keywords = meta?.tags || BLOG.KEYWORDS
return <Head>
<title>{title}</title>
<meta name='theme-color' content={BLOG.darkBackground} />
<meta name='theme-color' content={BLOG.BACKGROUND_DARK} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name='robots' content='follow, index' />
<meta charSet='UTF-8' />
{BLOG.seo.googleSiteVerification && (
<meta
name='google-site-verification'
content={BLOG.seo.googleSiteVerification}
/>
)}
{keywords && (
<meta name='keywords' content={keywords.join(', ')} />
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
<meta name='google-site-verification' content={BLOG.SEO_GOOGLE_SITE_VERIFICATION} />
)}
<meta name='keywords' content={keywords} />
<meta name='description' content={description} />
<meta property='og:locale' content={BLOG.lang} />
<meta property='og:locale' content={BLOG.LANG} />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:url' content={url}
@@ -36,13 +31,13 @@ const CommonHead = ({ meta }) => {
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
{meta.type === 'article' && (
{meta?.type === 'article' && (
<>
<meta
property='article:published_time'
content={meta.date || meta.createdTime}
/>
<meta property='article:author' content={BLOG.author} />
<meta property='article:author' content={BLOG.AUTHOR} />
</>
)}
</Head>

View File

@@ -7,7 +7,7 @@ import BLOG from '@/blog.config'
*/
const CommonScript = () => {
return (<>
{BLOG.comment?.DaoVoiceId && (<>
{BLOG.COMMENT_DAO_VOICE_ID && (<>
{/* DaoVoice 反馈 */}
<script async dangerouslySetInnerHTML={{
__html: `
@@ -18,7 +18,7 @@ const CommonScript = () => {
<script async dangerouslySetInnerHTML={{
__html: `
daovoice('init', {
app_id: "${BLOG.comment.DaoVoiceId}"
app_id: "${BLOG.COMMENT_DAO_VOICE_ID}"
});
daovoice('update');
`
@@ -26,85 +26,80 @@ const CommonScript = () => {
/>
</>)}
{/* GoogleAdsense 广告植入 */}
{BLOG.googleAdsenseId && (<script data-ad-client={BLOG.googleAdsenseId} async
{/* GoogleAdsense */}
{BLOG.ADSENSE_GOOGLE_ID && (<script data-ad-client={BLOG.ADSENSE_GOOGLE_ID} async
src='https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'/>)}
{BLOG.comment?.TidioId && (<>
{/* Tidio在线反馈 */}
<script async
src={`//code.tidio.co/${BLOG.comment.TidioId}.js`}
/>
</>)}
{BLOG.COMMENT_CUSDIS_APP_ID && <script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />}
{/* */}
{BLOG.gitter && (<>
{BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />}
{/* gitter聊天室 */}
{BLOG.COMMENT_GITTER_ROOM && (<>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer/>
<script async dangerouslySetInnerHTML={{
__html: `
((window.gitter = {}).chat = {}).options = {
room: 'tangly1024/community'
room: '${BLOG.COMMENT_GITTER_ROOM}'
};
`
}}/>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
</>)}
{/* 代码统计 */}
{BLOG.isProd && (<>
{/* ackee统计脚本 */}
{BLOG.ANALYTICS_ACKEE_TRACKER && (
<script async src={BLOG.ANALYTICS_ACKEE_TRACKER}
data-ackee-server={BLOG.ANALYTICS_ACKEE_DATA_SERVER}
data-ackee-domain-id={BLOG.ANALYTICS_ACKEE_DOMAIN_ID}
/>
)}
{/* ackee统计脚本 */}
{BLOG.analytics.provider === 'ackee' && (
<script async src={BLOG.analytics.ackeeConfig.tracker}
data-ackee-server={BLOG.analytics.ackeeConfig.dataAckeeServer}
data-ackee-domain-id={BLOG.analytics.ackeeConfig.domainId}
/>
)}
{/* 百度统计 */}
{BLOG.analytics.baiduAnalytics && (
<script async
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${BLOG.analytics.baiduAnalytics}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
{/* 百度统计 */}
{BLOG.ANALYTICS_BAIDU_ID && (
<script async
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${BLOG.ANALYTICS_BAIDU_ID}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`
}}
/>
)}
{/* 站长统计 */}
{BLOG.ANALYTICS_CNZZ_ID && (
<script async
dangerouslySetInnerHTML={{
__html: `
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${BLOG.ANALYTICS_CNZZ_ID}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${BLOG.ANALYTICS_CNZZ_ID}' type='text/javascript'%3E%3C/script%3E"));
`
}}
/>
)}
{/* 谷歌统计 */}
{BLOG.ANALYTICS_GOOGLE_ID && (<>
<script async
src={`https://www.googletagmanager.com/gtag/js?id=${BLOG.ANALYTICS_GOOGLE_ID}`}
/>
<script async
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${BLOG.ANALYTICS_GOOGLE_ID}', {
page_path: window.location.pathname,
});
`
}}
/>
)}
{/* 站长统计 */}
{BLOG.analytics.cnzzAnalytics && (
<script async
dangerouslySetInnerHTML={{
__html: `
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${BLOG.analytics.cnzzAnalytics}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${BLOG.analytics.cnzzAnalytics}' type='text/javascript'%3E%3C/script%3E"));
`
}}
/>
)}
{/* 谷歌统计 */}
{BLOG.analytics.provider === 'ga' && (<>
<script async
src={`https://www.googletagmanager.com/gtag/js?id=${BLOG.analytics.gaConfig.measurementId}`}
/>
<script async
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${BLOG.analytics.gaConfig.measurementId}', {
page_path: window.location.pathname,
});
`
}}
/>
</>)}
}}
/>
</>)}
</>)
}

View File

@@ -6,8 +6,7 @@ const Cusdis = ({ id, url, title }) => {
const anchor = document.getElementById('comments')
script.setAttribute(
'src',
BLOG.comment.cusdisConfig.scriptSrc ||
'https://cusdis.com/js/cusdis.es.js'
BLOG.COMMENT_CUSDIS_SCRIPT_SRC
)
script.setAttribute('async', true)
script.setAttribute('defer', true)
@@ -20,12 +19,12 @@ const Cusdis = ({ id, url, title }) => {
<div id="comments">
<div
id="cusdis_thread"
data-host={BLOG.comment.cusdisConfig.host || 'https://cusdis.com'}
data-app-id={BLOG.comment.cusdisConfig.appId}
data-host={BLOG.COMMENT_CUSDIS_HOST}
data-app-id={BLOG.COMMENT_CUSDIS_APP_ID}
data-page-id={id}
data-page-url={url}
data-page-title={title}
></div>
/>
</div>
)
}

53
components/Tabs.js Normal file
View File

@@ -0,0 +1,53 @@
import React, { useState } from 'react'
/**
* Tabs切换标签
* @param {*} param0
* @returns
*/
const Tabs = ({ children }) => {
if (!children) {
return <></>
}
let count = children.length
children.forEach(e => {
if (!e) {
count--
}
})
if (count === 1) {
return <section className='duration-200'>
{children}
</section>
}
const [currentTab, setCurrentTab] = useState(0)
function tabClickHandle (i) {
setCurrentTab(i)
}
return <div className='mb-5 bg-white dark:bg-gray-800 duration-200'>
<ul className='flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600'>
{children.map((item, index) => {
return <li key={index}
className={(currentTab === index ? 'font-black border-b-2 border-red-400 text-red-400 animate__animated animate__jello ' : 'font-extralight cursor-pointer') + ' text-sm font-sans '}
onClick={() => {
tabClickHandle(index)
}}>
{item?.key}
</li>
})}
</ul>
{children.map((item, index) => {
return <section key={index}
className={`${currentTab === index ? 'block animate__animated animate__fadeIn animate__faster' : 'hidden'}`}>
{item}
</section>
})}
</div>
}
export default Tabs

View File

@@ -11,9 +11,9 @@ import { useEffect } from 'react'
const Utterances = ({ issueTerm, layout }) => {
useEffect(() => {
const theme =
BLOG.appearance === 'auto'
BLOG.APPEARANCE === 'auto'
? 'preferred-color-scheme'
: BLOG.appearance === 'light'
: BLOG.APPEARANCE === 'light'
? 'github-light'
: 'github-dark'
const script = document.createElement('script')
@@ -21,7 +21,7 @@ const Utterances = ({ issueTerm, layout }) => {
script.setAttribute('src', 'https://utteranc.es/client.js')
script.setAttribute('crossorigin', 'anonymous')
script.setAttribute('async', true)
script.setAttribute('repo', BLOG.comment.utterancesConfig.repo)
script.setAttribute('repo', BLOG.COMMENT_UTTERRANCES_REPO)
script.setAttribute('issue-term', issueTerm)
script.setAttribute('theme', theme)
anchor.appendChild(script)
@@ -29,16 +29,9 @@ const Utterances = ({ issueTerm, layout }) => {
anchor.innerHTML = ''
}
})
return (
<>
<div
id="comments"
className={layout && layout === 'fullWidth' ? '' : 'md:-ml-16'}
>
<div className="utterances-frame"></div>
</div>
</>
)
return <div id="comments" >
<div className="utterances-frame h-auto w-auto"/>
</div>
}
export default Utterances

View File

@@ -1,7 +1,7 @@
const BLOG = require('../blog.config')
module.exports = function () {
switch (BLOG.lang.toLowerCase()) {
switch (BLOG.LANG.toLowerCase()) {
case 'zh-cn':
case 'zh-sg':
return 'SC'

View File

@@ -1,9 +1,8 @@
import BLOG from '@/blog.config'
export const GA_TRACKING_ID = BLOG.analytics.gaConfig.measurementId
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = url => {
window.gtag('config', GA_TRACKING_ID, {
window.gtag('config', BLOG.ANALYTICS_GOOGLE_ID, {
page_path: url
})
}

View File

@@ -60,7 +60,7 @@ export async function getAllPosts ({ notionPageData, from, includePage = false }
})
// Sort by date
if (BLOG.sortByDate) {
if (BLOG.POSTS_SORT_BY === 'date') {
posts.sort((a, b) => {
const dateA = new Date(a?.date?.start_date || a.createdTime)
const dateB = new Date(b?.date?.start_date || b.createdTime)

View File

@@ -13,13 +13,13 @@ import { getAllTags } from './getAllTags'
* @param latestPostCount 截取最新文章数量
* @param tagsCount 截取标签数量
* @param includePage 是否包含PAGE类型
* @returns { allPosts: '文章列表', latestPosts ’最新文章, categories分类列表 postCount:'文章总数'tags:'标签列表' }
* @returns {}
* allPosts 所有博客
* categories 所有分类
* tags 所有标签
*/
export async function getGlobalNotionData ({
pageId = BLOG.notionPageId,
pageId = BLOG.NOTION_PAGE_ID,
from,
latestPostCount = 5,
tagsCount = 16,

View File

@@ -9,7 +9,7 @@ export async function getPostBlocks (id, from, slice) {
console.log('[请求缓存]:', `from:${from}`, `id:${id}`)
return filterPostBlocks(id, pageBlock, slice)
}
const authToken = BLOG.notionAccessToken || null
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({ authToken })
try {
console.log('[请求API]:', `from:${from}`, `id:${id}`)
@@ -57,7 +57,7 @@ function filterPostBlocks (id, pageBlock, slice) {
}
// 去掉不用的字段
if (id === BLOG.notionPageId) {
if (id === BLOG.NOTION_PAGE_ID) {
return clonePageBlock
}
return clonePageBlock

View File

@@ -4,25 +4,25 @@ import BLOG from '@/blog.config'
export function generateRss (posts) {
const year = new Date().getFullYear()
const feed = new Feed({
title: BLOG.title,
description: BLOG.description,
id: `${BLOG.link}/${BLOG.path}`,
link: `${BLOG.link}/${BLOG.path}`,
language: BLOG.lang,
favicon: `${BLOG.link}/favicon.png`,
copyright: `All rights reserved ${year}, ${BLOG.author}`,
author: {
name: BLOG.author,
email: BLOG.email,
link: BLOG.link
TITLE: BLOG.TITLE,
DESCRIPTION: BLOG.DESCRIPTION,
id: `${BLOG.LINK}/${BLOG.PATH}`,
LINK: `${BLOG.LINK}/${BLOG.PATH}`,
language: BLOG.LANG,
favicon: `${BLOG.LINK}/favicon.png`,
copyright: `All rights reserved ${year}, ${BLOG.AUTHOR}`,
AUTHOR: {
name: BLOG.AUTHOR,
email: BLOG.CONTACT_EMAIL,
link: BLOG.LINK
}
})
posts.forEach(post => {
feed.addItem({
title: post.title,
id: `${BLOG.link}/article/${post.slug}`,
link: `${BLOG.link}/article/${post.slug}`,
description: post.summary,
TITLE: post.title,
id: `${BLOG.LINK}/article/${post.slug}`,
LINK: `${BLOG.LINK}/article/${post.slug}`,
DESCRIPTION: post.summary,
date: new Date(post?.date?.start_date || post.createdTime)
})
})

View File

@@ -1,7 +1,7 @@
const BLOG = require('./blog.config')
module.exports = {
siteUrl: BLOG.link,
siteUrl: BLOG.LINK,
changefreq: 'daily',
priority: 0.7,
generateRobotsTxt: true,

View File

@@ -26,15 +26,10 @@ const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr:
const MyApp = ({ Component, pageProps }) => {
return (
<GlobalContextProvider>
{BLOG.isProd && BLOG?.analytics?.provider === 'ackee' && (
<Ackee
ackeeServerUrl={BLOG.analytics.ackeeConfig.dataAckeeServer}
ackeeDomainId={BLOG.analytics.ackeeConfig.domainId}
/>
)}
{BLOG.isProd && BLOG?.analytics?.provider === 'ga' && <Gtag />}
{BLOG.analytics.busuanzi && <Busuanzi/>}
{BLOG.googleAdsenseId && <GoogleAdsense/>}
{BLOG.ANALYTICS_ACKEE_TRACKER && <Ackee />}
{BLOG.ANALYTICS_GOOGLE_ID && <Gtag />}
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <Busuanzi/>}
{BLOG.ADSENSE_GOOGLE_ID && <GoogleAdsense/>}
<Component {...pageProps} />
</GlobalContextProvider>
)

View File

@@ -11,14 +11,14 @@ class MyDocument extends Document {
render () {
return (
<Html lang={BLOG.lang}>
<Html lang={BLOG.LANG}>
<Head>
<link rel='icon' href='/favicon.ico' />
<link rel='icon' href='/favicon.svg' type='image/svg+xml' />
<CommonScript />
</Head>
<body className={`${BLOG.font} bg-day dark:bg-night duration-200`}>
<body className={`${BLOG.FONT} bg-day dark:bg-night`}>
<Main />
<NextScript />
</body>

View File

@@ -13,7 +13,7 @@ const Slug = (props) => {
if (!props.post) {
return <Custom404 />
}
return <LayoutSlug {...props}/>
return <LayoutSlug {...props} />
}
export async function getStaticPaths () {
@@ -24,8 +24,8 @@ export async function getStaticPaths () {
}
}
const from = '[slug-paths'
const { allPosts } = await getGlobalNotionData({ from, includePage: false })
const from = 'slug-paths'
const { allPosts } = await getGlobalNotionData({ from, includePage: true })
return {
paths: allPosts.map(row => ({ params: { slug: row.slug } })),
fallback: true
@@ -35,7 +35,7 @@ export async function getStaticPaths () {
export async function getStaticProps ({ params: { slug } }) {
const from = `slug-props-${slug}`
const { allPosts, categories, tags, postCount, latestPosts } =
await getGlobalNotionData({ from, includePage: false })
await getGlobalNotionData({ from, includePage: true })
const post = allPosts.find(p => p.slug === slug)
@@ -45,10 +45,10 @@ export async function getStaticProps ({ params: { slug } }) {
post.blockMap = await getPostBlocks(post.id, 'slug')
// 上一篇、下一篇文章关联
const index = allPosts.indexOf(post)
const prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
const next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
const posts = allPosts.filter(post => post?.type?.[0] === 'Post')
const index = posts.indexOf(post)
const prev = posts.slice(index - 1, index)[0] ?? posts.slice(-1)[0]
const next = posts.slice(index + 1, index + 2)[0] ?? posts[0]
const recommendPosts = getRecommendPost(post, allPosts)
@@ -80,7 +80,7 @@ function getRecommendPost (post, allPosts, count = 5) {
if (post.tags && post.tags.length) {
const currentTag = post.tags[0]
filteredPosts = filteredPosts.filter(
p => p && p.tags && p.tags.includes(currentTag) && p.slug !== post.slug
p => p && p.tags && p.tags.includes(currentTag) && p.slug !== post.slug && p.type === 'post'
)
}
shuffleSort(filteredPosts)

View File

@@ -1,13 +1,10 @@
import { getAllPosts } from '@/lib/notion'
import { generateRss } from '@/lib/rss'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
export async function getServerSideProps ({ res }) {
res.setHeader('Content-Type', 'text/xml')
let posts = await getAllPosts({ from: 'feed' })
posts = posts
.filter(post => post.status[0] === 'Published' && post.type[0] === 'Post')
.slice(0, 10)
const xmlFeed = generateRss(posts)
const globalNotionData = await getGlobalNotionData({ from: 'rss' })
const xmlFeed = generateRss(globalNotionData?.allPosts?.slice(0, 10) || [])
res.write(xmlFeed)
res.end()
return {

View File

@@ -11,24 +11,24 @@ export async function getStaticProps () {
const from = 'index'
const { allPosts, latestPosts, categories, tags, postCount } = await getGlobalNotionData({ from })
const meta = {
title: `${BLOG.title}`,
description: BLOG.description,
title: `${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
// 处理分页
const page = 1
let postsToShow
if (BLOG.postListStyle !== 'page') {
if (BLOG.POST_LIST_STYLE !== 'page') {
postsToShow = Array.from(allPosts)
} else {
postsToShow = allPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
BLOG.POSTS_PER_PAGE * (page - 1),
BLOG.POSTS_PER_PAGE * page
)
for (const i in postsToShow) {
const post = postsToShow[i]
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.home.previewLines)
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
if (blockMap) {
post.blockMap = blockMap
}

View File

@@ -14,7 +14,7 @@ const Page = (props) => {
export async function getStaticPaths () {
const from = 'page-paths'
const { postCount } = await getGlobalNotionData({ from })
const totalPages = Math.ceil(postCount / BLOG.postsPerPage)
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
return {
// remove first page, we 're not gonna handle that.
paths: Array.from({ length: totalPages - 1 }, (_, i) => ({ params: { page: '' + (i + 2) } })),
@@ -32,19 +32,20 @@ export async function getStaticProps ({ params: { page } }) {
postCount
} = await getGlobalNotionData({ from })
const meta = {
title: `${page} | Page | ${BLOG.title}`,
description: BLOG.description,
title: `${page} | Page | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
// 处理分页
const postsToShow = allPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
BLOG.POSTS_PER_PAGE * (page - 1),
BLOG.POSTS_PER_PAGE * page
)
for (const i in postsToShow) {
const post = postsToShow[i]
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.home.previewLines)
const blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
if (blockMap) {
post.blockMap = blockMap
}

View File

@@ -9,7 +9,7 @@ const fontSerifCJK = !CJK()
: [`"Noto Serif CJK ${CJK()}"`, `"Noto Serif ${CJK()}"`]
module.exports = {
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.js'],
darkMode: BLOG.appearance === 'class' ? 'media' : 'class', // or 'media' or 'class'
darkMode: BLOG.APPEARANCE === 'class' ? 'media' : 'class', // or 'media' or 'class'
theme: {
fontFamily: {
sans: ['"IBM Plex Sans"', ...fontFamily.sans, ...fontSansCJK],
@@ -26,10 +26,10 @@ module.exports = {
extend: {
colors: {
day: {
DEFAULT: BLOG.lightBackground || '#ffffff'
DEFAULT: BLOG.BACKGROUND_LIGHT || '#ffffff'
},
night: {
DEFAULT: BLOG.darkBackground || '#111827'
DEFAULT: BLOG.BACKGROUND_DARK || '#111827'
}
}
}

View File

@@ -0,0 +1,6 @@
export const Layout404 = () => {
return <div>
404 Not found.
</div>
}

View File

@@ -0,0 +1,5 @@
export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
return <div>
Archive Page
</div>
}

View File

@@ -0,0 +1,40 @@
import CommonHead from '@/components/CommonHead'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param children
* @param layout
* @param tags
* @param meta
* @param post
* @param currentSearch
* @param currentCategory
* @param currentTag
* @param categories
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = ({
children,
headerSlot,
tags,
meta,
post,
postCount,
sideBarSlot,
floatSlot,
rightAreaSlot,
currentSearch,
currentCategory,
currentTag,
categories
}) => {
return (<>
<CommonHead meta={meta} />
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
{children}
</main>
</>)
}
export default LayoutBase

View File

@@ -0,0 +1,5 @@
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
return <div>
Category - {category}
</div>
}

View File

@@ -0,0 +1,11 @@
export const LayoutCategoryIndex = ({
tags,
allPosts,
categories,
postCount,
latestPosts
}) => {
return <div>
CategoryIndex
</div>
}

View File

@@ -0,0 +1,3 @@
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
return <div>Index</div>
}

View File

@@ -0,0 +1,5 @@
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
return <div>
Page - {page}
</div>
}

View File

@@ -0,0 +1,34 @@
import { useRouter } from 'next/router'
export const LayoutSearch = ({
posts,
tags,
categories,
postCount
}) => {
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = posts
}
console.log(filteredPosts)
return <div>
Search {searchKey}
</div>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

View File

@@ -0,0 +1,21 @@
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
export const LayoutSlug = ({
post,
tags,
prev,
next,
recommendPosts,
categories,
postCount,
latestPosts
}) => {
return <div>
Slug
</div>
}

View File

@@ -0,0 +1,5 @@
export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts }) => {
return <div>
Tag - {tag}
</div>
}

View File

@@ -0,0 +1,5 @@
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
return <div>
TagIndex
</div>
}

View File

10
themes/Empty/index.js Normal file
View File

@@ -0,0 +1,10 @@
export { LayoutIndex } from './LayoutIndex'
export { LayoutSearch } from './LayoutSearch'
export { LayoutArchive } from './LayoutArchive'
export { LayoutSlug } from './LayoutSlug'
export { Layout404 } from './Layout404'
export { LayoutCategory } from './LayoutCategory'
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
export { LayoutPage } from './LayoutPage'
export { LayoutTag } from './LayoutTag'
export { LayoutTagIndex } from './LayoutTagIndex'

View File

@@ -0,0 +1,5 @@
import LayoutBase from './LayoutBase'
export const Layout404 = (props) => {
return <LayoutBase {...props}>404</LayoutBase>
}

View File

@@ -0,0 +1,61 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { useEffect } from 'react'
import BlogArchiveItem from './components/BlogPostArchive'
import LayoutBase from './LayoutBase'
export const LayoutArchive = (props) => {
const { locale } = useGlobal()
const { posts } = props
// 深拷贝
const postsSortByDate = Object.create(posts)
// 时间排序
postsSortByDate.sort((a, b) => {
const dateA = new Date(a?.date.start_date || a.createdTime)
const dateB = new Date(b?.date.start_date || b.createdTime)
return dateB - dateA
})
const meta = {
title: `${locale.NAV.ARCHIVE} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
const archivePosts = {}
postsSortByDate.forEach(post => {
const date = post.date.start_date.slice(0, 7)
if (archivePosts[date]) {
archivePosts[date].push(post)
} else {
archivePosts[date] = [post]
}
})
useEffect(() => {
if (window) {
const anchor = window.location.hash
if (anchor) {
setTimeout(() => {
const anchorElement = document.getElementById(anchor.substring(1))
if (anchorElement) {
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
}, 300)
}
}
}, [])
return <LayoutBase {...props} meta={meta}>
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
{Object.keys(archivePosts).map(archiveTitle => (
<BlogArchiveItem
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,45 @@
import CommonHead from '@/components/CommonHead'
import TopNav from './components/TopNav'
import AsideLeft from './components/AsideLeft'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param children
* @param layout
* @param tags
* @param meta
* @param post
* @param currentSearch
* @param currentCategory
* @param currentTag
* @param categories
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = (props) => {
const {
children,
headerSlot,
tags,
meta,
currentCategory,
currentTag,
categories
} = props
return (<>
<CommonHead meta={meta} />
<TopNav {...props}/>
<div className='flex'>
<AsideLeft tags={tags} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
<main id='wrapper' className='flex w-full py-8 justify-center'>
<div className='2xl:max-w-6xl md:max-w-3xl w-full'>
<div> {headerSlot} </div>
<div>{children}</div>
</div>
</main>
</div>
</>)
}
export default LayoutBase

View File

@@ -0,0 +1,8 @@
import BlogListPage from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutCategory = (props) => {
return <LayoutBase {...props}>
<BlogListPage page={props.page} posts={props.posts} postCount={props.postCount} />
</LayoutBase>
}

View File

@@ -0,0 +1,32 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faFolder, faTh } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutCategoryIndex = (props) => {
const { locale } = useGlobal()
const { categories } = props
const meta = {
title: `${locale.COMMON.CATEGORY} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase {...props} meta={meta}>
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div className='dark:text-gray-200 mb-5'>
<FontAwesomeIcon icon={faTh} className='mr-4' />{locale.COMMON.CATEGORY}:
</div>
<div id='category-list' className='duration-200 flex flex-wrap'>
{Object.keys(categories).map(category => {
return <Link key={category} href={`/category/${category}`} passHref>
<div
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})
</div>
</Link>
})}
</div>
</div> </LayoutBase>
}

View File

@@ -0,0 +1,10 @@
import BlogListPage from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutIndex = (props) => {
return <LayoutBase {...props}>
<BlogListPage posts={props.posts} postCount={props.postCount}/>
</LayoutBase>
}

View File

@@ -0,0 +1,10 @@
import BlogListPage from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutPage = (props) => {
return <LayoutBase {...props}>
<BlogListPage page={props.page} posts={props.posts} postCount={props.postCount}/>
</LayoutBase>
}

View File

@@ -0,0 +1,30 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
export const LayoutSearch = (props) => {
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = props.posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = props.posts
}
console.log(filteredPosts)
return <LayoutBase {...props}>
Search {searchKey}
</LayoutBase>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

View File

@@ -0,0 +1,25 @@
import BLOG from '@/blog.config'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import ArticleDetail from './components/ArticleDetail'
import LayoutBase from './LayoutBase'
export const LayoutSlug = (props) => {
const { post } = props
const meta = {
title: `${post.title} | ${BLOG.TITLE}`,
description: post.summary,
type: 'article',
tags: post.tags
}
return (
<LayoutBase meta={meta} {...props} >
<ArticleDetail {...props} />
</LayoutBase>
)
}

View File

@@ -0,0 +1,8 @@
import BlogListPage from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutTag = (props) => {
return <LayoutBase {...props}>
<BlogListPage {...props} />
</LayoutBase>
}

View File

@@ -0,0 +1,27 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import TagItem from './components/TagItem'
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = (props) => {
const { locale } = useGlobal()
const { tags } = props
const meta = {
title: `${locale.COMMON.TAGS} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase {...props} meta={meta}>
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTag} className='mr-4'/>{locale.COMMON.TAGS}:</div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{ tags.map(tag => {
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
}) }
</div>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,26 @@
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleAround ({ prev, next }) {
if (!prev || !next) {
return <></>
}
return <section className='text-gray-800 h-28 flex items-center justify-between space-x-5 my-4'>
<Link href={`/article/${prev.slug}`} passHref>
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
</a>
</Link>
<Link href={`/article/${next.slug}`} passHref>
<a className='text-sm cursor-pointer justify-center items-center flex w-full h-full bg-gray-400 bg-opacity-40 hover:bg-gray-700 hover:text-white duration-300'>{next.title}
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
</a>
</Link>
</section>
}

View File

@@ -0,0 +1,156 @@
import Comment from '@/components/Comment'
import formatDate from '@/lib/formatDate'
import { useGlobal } from '@/lib/global'
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import mediumZoom from 'medium-zoom'
import Link from 'next/link'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { useEffect, useRef } from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import ArticleAround from './ArticleAround'
/**
*
* @param {*} param0
* @returns
*/
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
const zoom = typeof window !== 'undefined' && mediumZoom({
container: '.notion-viewport',
background: 'rgba(0, 0, 0, 0.2)',
margin: getMediumZoomMargin()
})
const zoomRef = useRef(zoom ? zoom.clone() : null)
useEffect(() => {
// 将所有container下的所有图片添加medium-zoom
const container = document.getElementById('container')
const imgList = container.getElementsByTagName('img')
if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
(zoomRef.current).attach(imgList[i])
}
}
})
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
{post.type && !post.type.includes('Page') && post?.page_cover && (
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
</div>
)}
<article itemScope itemType="https://schema.org/Movie"
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-32 dark:border-gray-700 bg-white dark:bg-gray-800"
>
<header className='animate__slideInDown animate__animated'>
{/* 文章Title */}
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
{post.title}
</div>
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
<div>
<Link href={`/category/${post.category}`} passHref>
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
{post.category}
</a>
</Link>
<span className='mr-2'>|</span>
{post.type[0] !== 'Page' && (<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
>
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
{date}
</a>
</Link>
<span className='mr-2'>|</span>
</>)}
<div className="hidden busuanzi_container_page_pv font-light mr-2">
<FontAwesomeIcon icon={faEye} className='mr-1'/>
&nbsp;
<span className="mr-2 busuanzi_value_page_pv"/>
<span className='mr-2'>|</span>
</div>
</div>
</section>
</header>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
</section>
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
{/* 文章内嵌广告 */}
<ins className="adsbygoogle"
style={{ display: 'block', textAlign: 'center' }}
data-adtest="on"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-2708419466378217"
data-ad-slot="3806269138"/>
</section>
</article>
<ArticleAround prev={prev} next={next}/>
{/* 评论互动 */}
<div className="duration-200 shadow px-12 w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
<div className='text-2xl mt-8 mx-8'>发表评论</div>
<Comment frontMatter={post} />
</div>
</div>)
}
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
function getMediumZoomMargin () {
const width = window.innerWidth
if (width < 500) {
return 8
} else if (width < 800) {
return 20
} else if (width < 1280) {
return 30
} else if (width < 1600) {
return 40
} else if (width < 1920) {
return 48
} else {
return 72
}
}

View File

@@ -0,0 +1,47 @@
import BLOG from '@/blog.config'
import Logo from './Logo'
import GroupCategory from './GroupCategory'
import GroupMenu from './GroupMenu'
import GroupTag from './GroupTag'
import SearchInput from './SearchInput'
import SiteInfo from './SiteInfo'
function AsideLeft ({ tags, currentTag, categories, currentCategory }) {
return <div className='w-72 bg-white min-h-screen px-10 py-14 hidden lg:block'>
<Logo />
<section className='flex flex-col text-gray-600'>
<hr className='w-12 my-8' />
<GroupMenu/>
</section>
<section className='flex flex-col text-gray-600'>
<hr className='w-12 my-8' />
<SearchInput/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
{ BLOG.DESCRIPTION }
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<GroupTag tags={tags} currentTag={currentTag}/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<GroupCategory categories={categories} currentCategory={currentCategory}/>
</section>
<section className='flex flex-col'>
<hr className='w-12 my-8 ' />
<SiteInfo/>
</section>
</div>
}
export default AsideLeft

View File

@@ -0,0 +1,39 @@
import BLOG from '@/blog.config'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import CONFIG_FUKA from '../config_fuka'
import Card from './Card'
const BlogCard = ({ post, showSummary }) => {
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
return (
<Card className='w-full lg:max-w-sm p-2'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
<div className='p-2 flex flex-col w-full'>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={`cursor-pointer font-bold hover:underline text-xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
{post.title}
</a>
</Link>
{(!showPreview || showSummary) && <p className='mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden'>
{post.summary}
</p>}
</div>
{CONFIG_FUKA.POST_LIST_COVER && post?.page_cover && (
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<div className='h-40 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
</div>
</Link>
)}
</div >
</Card>
)
}
export default BlogCard

View File

@@ -0,0 +1,12 @@
/**
* 空白博客 列表
* @returns {JSX.Element}
* @constructor
*/
const BlogListEmpty = ({ currentSearch }) => {
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
</div>
}
export default BlogListEmpty

View File

@@ -0,0 +1,55 @@
import BLOG from '@/blog.config'
import { useEffect, useState } from 'react'
import BlogCard from './BlogCard'
import BlogPostListEmpty from './BlogListEmpty'
import PaginationSimple from './PaginationSimple'
/**
* 文章列表分页表格
* @param page 当前页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogListPage = ({ page = 1, posts = [], postCount }) => {
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const showNext = page < totalPage && posts.length < postCount
const [colCount, changeCol] = useState(3)
function updateCol () {
if (window.outerWidth > 1200) {
changeCol(3)
} else {
changeCol(1)
}
}
useEffect(() => {
updateCol()
window.addEventListener('resize', updateCol)
return () => {
window.removeEventListener('resize', updateCol)
}
})
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />
} else {
return (
<div id="container">
{/* 文章列表 */}
<div style={{ columnCount: colCount }}>
{posts.map(post => (
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}>
<BlogCard key={post.id} post={post} />
</div>
))}
</div>
<PaginationSimple page={page} showNext={showNext} />
</div>
)
}
}
export default BlogListPage

View File

@@ -0,0 +1,32 @@
import React from 'react'
import Link from 'next/link'
import BLOG from '@/blog.config'
/**
* 博客归档
* @param posts 所有文章
* @param archiveTitle 归档标题
* @returns {JSX.Element}
* @constructor
*/
const BlogArchiveItem = ({ posts = [], archiveTitle }) => {
if (!posts || posts.length === 0) {
return <></>
} else {
return <div>
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
<ul>
{posts.map(post => (
<li key={post.id} className='border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500'>
<div id={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span> &nbsp;
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className='dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600'>{post.title}</a>
</Link>
</div>
</li>
))}
</ul>
</div>
}
}
export default BlogArchiveItem

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
<>{headerSlot}</>
<section className="shadow mb-4 p-2 bg-white dark:bg-gray-800 hover:shadow-lg duration-200">
{children}
</section>
</div>
}
export default Card

View File

@@ -0,0 +1,38 @@
import React, { useEffect, useRef } from 'react'
const Collapse = props => {
const collapseRef = useRef(null)
const collapseSection = element => {
const sectionHeight = element.scrollHeight
requestAnimationFrame(function () {
element.style.height = sectionHeight + 'px'
requestAnimationFrame(function () {
element.style.height = 0 + 'px'
})
})
}
const expandSection = element => {
const sectionHeight = element.scrollHeight
element.style.height = sectionHeight + 'px'
const clearTime = setTimeout(() => {
element.style.height = 'auto'
}, 400)
clearTimeout(clearTime)
}
useEffect(() => {
const element = collapseRef.current
if (props.isOpen) {
expandSection(element)
} else {
collapseSection(element)
}
}, [props.isOpen])
return (
<div ref={collapseRef} style={{ height: '0px' }} className='overflow-hidden duration-200'>
{props.children}
</div>
)
}
Collapse.defaultProps = { isOpen: false }
export default Collapse

View File

@@ -0,0 +1,28 @@
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
function GroupCategory ({ currentCategory, categories }) {
if (!categories) {
return <></>
}
return <>
<div id='category-list' className='dark:border-gray-600 flex flex-wrap'>
{Object.keys(categories).map(category => {
const selected = currentCategory === category
return <Link key={category} href={`/category/${category}`} passHref>
<a className={(selected
? 'hover:text-white dark:hover:text-white bg-gray-600 text-white '
: 'dark:text-gray-400 text-gray-500 hover:text-white hover:bg-gray-500 dark:hover:text-white') +
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
<FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={`${selected ? 'text-white' : 'text-gray-400'} mr-2`} />{category}({categories[category]})
</a>
</Link>
})}
</div>
</>
}
export default GroupCategory

View File

@@ -0,0 +1,38 @@
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import CONFIG_FUKA from '../config_fuka'
function GroupMenu () {
const { locale } = useGlobal()
const router = useRouter()
const links = [
{ id: 0, name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ id: 1, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_FUKA.MENU_CATEGORY },
{ id: 2, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_FUKA.MENU_TAG },
{ id: 3, name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_FUKA.MENU_ARCHIVE },
{ id: 4, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_FUKA.MENU_ABOUT }
]
return <nav id='nav' className='font-sans text-sm'>
{links.map(link => {
if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
<a className={'py-0.5 duration-500 justify-between text-gray-500 hover:text-black cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'text-black' : ' ')} >
<div className='my-auto items-center justify-center flex '>
<div className={ 'text-gray-500 hover:text-black'}>{link.name}</div>
</div>
{link.slot}
</a>
</Link>
} else {
return null
}
})}
</nav>
}
export default GroupMenu

View File

@@ -0,0 +1,24 @@
import TagItemMini from './TagItemMini'
/**
* 标签组
* @param tags
* @param currentTag
* @returns {JSX.Element}
* @constructor
*/
function GroupTag ({ tags, currentTag }) {
if (!tags) return <></>
return (
<div id='tags-group' className='dark:border-gray-600 w-66 space-y-2'>
{
tags.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,12 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
function Logo () {
return <section className='flex'>
<Link href='/'>
<a className='hover:bg-black hover:text-white border-black border-2 duration-500 px-4 py-2 cursor-pointer font-black'>{BLOG.TITLE}</a>
</Link>
</section>
}
export default Logo

View File

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

View File

@@ -0,0 +1,68 @@
import { useRouter } from 'next/router'
import { useImperativeHandle, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
const [searchKey, setSearchKey] = useState(currentSearch || '')
const [onLoading, setLoadingState] = useState(false)
const router = useRouter()
const searchInputRef = useRef()
useImperativeHandle(cRef, () => {
return {
focus: () => {
searchInputRef?.current?.focus()
}
}
})
const handleSearch = (key) => {
if (key && key !== '') {
setLoadingState(true)
router.push({ pathname: '/search', query: { s: key } }).then(r => {
setLoadingState(false)
})
} 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 = ''
setSearchKey('')
}
const updateSearchKey = (val) => {
setSearchKey(val)
}
return <div className='flex w-full bg-gray-100'>
<input
ref={searchInputRef}
type='text'
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
onKeyUp={handleKeyUp}
onChange={e => updateSearchKey(e.target.value)}
defaultValue={searchKey}
/>
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}>
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
</div>
{(searchKey && searchKey.length &&
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
</div>
)}
</div>
}
export default SearchInput

View File

@@ -0,0 +1,26 @@
import BLOG from '@/blog.config'
import { faEye, faShieldAlt, faUsers } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
function SiteInfo ({ title }) {
const d = new Date()
const currentYear = d.getFullYear()
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
return (
<footer
className='leading-6 justify-start w-full text-gray-400 text-xs font-sans'
>
<span> © {`${startYear}${currentYear}`} <span> <a href={BLOG.LINK} className='text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>. <br /></span>
<span>Powered by <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span><br /></span>
{BLOG.BEI_AN && <><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
<span className='hidden busuanzi_container_site_pv'> <FontAwesomeIcon icon={faEye} /><span className='px-1 busuanzi_value_site_pv'> </span> </span>
<span className='pl-2 hidden busuanzi_container_site_uv'> <FontAwesomeIcon icon={faUsers} /> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
<br />
<h1>{title}</h1>
</footer>
)
}
export default SiteInfo

View File

@@ -0,0 +1,26 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
import { useGlobal } from '@/lib/global'
const TagItem = ({ tag, selected }) => {
const { locale } = useGlobal()
if (!tag) {
<div> { locale.COMMON.NOTAG } </div>
}
return (
<Link href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<li
className={`notion-${tag.color}_background dark:bg-gray-700 list-none cursor-pointer rounded-md
duration-200 mr-1 my-1 px-2 py-1 text-sm whitespace-nowrap
hover:bg-gray-200 dark:hover:bg-gray-800 `}>
<div className='text-gray-600 dark:text-gray-300 dark:hover:text-white'>
{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {`${tag.name} `} {tag.count ? `(${tag.count})` : ''}
</div>
</li>
</Link>
)
}
export default TagItem

View File

@@ -0,0 +1,17 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => {
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-gray-900'
: `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'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a>
</Link>
}
export default TagItemMini

View File

@@ -0,0 +1,51 @@
import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useState } from 'react'
import Collapse from './Collapse'
import GroupMenu from './GroupMenu'
import Logo from './Logo'
/**
* 顶部导航
* @param {*} param0
* @returns
*/
const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => {
const [isOpen, changeShow] = useState(false)
const toggleMenuOpen = () => {
changeShow(!isOpen)
}
return (<div id='top-nav' className='z-40 block lg:hidden'>
{/* 导航栏 */}
<div id='sticky-nav' className={'lg:relative w-full top-0 z-20 transform duration-500'}>
<Collapse isOpen={isOpen}>
<div className='bg-white py-1 px-5'>
<GroupMenu/>
</div>
</Collapse>
<div className='w-full flex justify-between items-center p-4 bg-white'>
{/* 左侧LOGO 标题 */}
<div className='flex flex-none flex-grow-0'>
<Logo/>
</div>
<div className='flex'>
</div>
{/* 右侧功能 */}
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<div onClick={toggleMenuOpen} className='w-18 cursor-pointer'>
菜单 { isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
</div>
</div>
</div>
</div>
</div>)
}
export default TopNav

View File

@@ -0,0 +1,12 @@
const FUKA_CONFIG = {
POST_LIST_COVER: true, // 文章列表显示图片封面
// 菜单
MENU_ABOUT: true, // 显示关于
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索
}
export default FUKA_CONFIG

10
themes/Fukasawa/index.js Normal file
View File

@@ -0,0 +1,10 @@
export { LayoutIndex } from './LayoutIndex'
export { LayoutSearch } from './LayoutSearch'
export { LayoutArchive } from './LayoutArchive'
export { LayoutSlug } from './LayoutSlug'
export { Layout404 } from './Layout404'
export { LayoutCategory } from './LayoutCategory'
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
export { LayoutPage } from './LayoutPage'
export { LayoutTag } from './LayoutTag'
export { LayoutTagIndex } from './LayoutTagIndex'

View File

@@ -21,7 +21,7 @@ export const Layout404 = () => {
}, 30000000)
})
return <LayoutBase meta={{ title: `${BLOG.title} | 页面找不到啦` }}>
return <LayoutBase meta={{ title: `${BLOG.TITLE} | 页面找不到啦` }}>
<div
className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
<div className='dark:text-gray-200'>

View File

@@ -18,8 +18,8 @@ export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
})
const meta = {
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
description: BLOG.description,
title: `${locale.NAV.ARCHIVE} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}

View File

@@ -1,4 +1,3 @@
import BLOG from '@/blog.config'
import CommonHead from '@/components/CommonHead'
import FloatDarkModeButton from './components/FloatDarkModeButton'
import Footer from './components/Footer'
@@ -12,6 +11,7 @@ import { useGlobal } from '@/lib/global'
import PropTypes from 'prop-types'
import React, { useEffect, useRef, useState } from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import CONFIG_NEXT from './config_next'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -79,7 +79,7 @@ const LayoutBase = ({
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
<SideAreaLeft targetRef={targetRef} post={post} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
<section id='center' className={`${BLOG.topNavType !== 'normal' ? 'mt-14' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
<section id='center' className={`${CONFIG_NEXT.NAV_TYPE !== 'normal' ? 'mt-40' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
{onLoading
? <LoadingCover/>
: <>
@@ -91,7 +91,7 @@ const LayoutBase = ({
</main>
{/* 右下角悬浮 */}
<div className='right-8 bottom-10 lg:right-2 lg:bottom-2 fixed justify-end z-20 font-sans'>
<div className='right-8 bottom-12 lg:right-2 fixed justify-end z-20 font-sans'>
<div className={(show ? 'animate__animated ' : 'hidden') + ' animate__fadeInUp rounded-md glassmorphism justify-center duration-500 animate__faster flex space-x-2 items-center cursor-pointer '}>
<JumpToTopButton percent={percent}/>
<JumpToBottomButton />

View File

@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import LayoutBase from '@/themes/NEXT/LayoutBase'
import LayoutBase from './LayoutBase'
import StickyBar from './components/StickyBar'
import CategoryList from './components/CategoryList'
import BlogPostListScroll from './components/BlogPostListScroll'
@@ -8,8 +8,8 @@ import BlogPostListScroll from './components/BlogPostListScroll'
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
const { locale } = useGlobal()
const meta = {
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase meta={meta} tags={tags} currentCategory={category} postCount={postCount} latestPosts={latestPosts} categories={categories}>

View File

@@ -14,8 +14,8 @@ export const LayoutCategoryIndex = ({
}) => {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
title: `${locale.COMMON.CATEGORY} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase meta={meta} totalPosts={allPosts} tags={tags} postCount={postCount} latestPosts={latestPosts}>

View File

@@ -4,7 +4,7 @@ import LatestPostsGroup from './components/LatestPostsGroup'
import Card from './components/Card'
import BlogPostListScroll from './components/BlogPostListScroll'
import BlogPostListPage from './components/BlogPostListPage'
import { CONFIG_NEXT } from './index'
import CONFIG_NEXT from './config_next'
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
return <LayoutBase
@@ -18,7 +18,7 @@ export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPo
postCount={postCount}
categories={categories}
>
{CONFIG_NEXT.POSTS_LIST_TYPE !== 'page'
{CONFIG_NEXT.POST_LIST_TYPE !== 'page'
? (
<BlogPostListScroll posts={posts} tags={tags} showSummary={true} />
)

View File

@@ -1,7 +1,7 @@
import LayoutBase from './LayoutBase'
import LatestPostsGroup from './components/LatestPostsGroup'
import BlogPostListPage from './components/BlogPostListPage'
import { CONFIG_NEXT } from './index'
import CONFIG_NEXT from './config_next'
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
return (

View File

@@ -22,8 +22,8 @@ export const LayoutSearch = ({ posts, tags, categories, postCount }) => {
const { locale } = useGlobal()
const meta = {
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.title} `,
description: BLOG.description,
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.TITLE} `,
description: BLOG.DESCRIPTION,
type: 'website'
}
return (

View File

@@ -8,13 +8,7 @@ import ArticleDetail from './components/ArticleDetail'
import TocDrawer from './components/TocDrawer'
import Live2D from './components/Live2D'
import { useRef } from 'react'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { CONFIG_NEXT } from './index'
import CONFIG_NEXT from './config_next'
export const LayoutSlug = ({
post,
@@ -27,7 +21,7 @@ export const LayoutSlug = ({
latestPosts
}) => {
const meta = {
title: `${post.title} | ${BLOG.title}`,
title: `${post.title} | ${BLOG.TITLE}`,
description: post.summary,
type: 'article',
tags: post.tags
@@ -35,8 +29,10 @@ export const LayoutSlug = ({
const drawerRight = useRef(null)
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
post.content = Object.keys(post?.blockMap?.block)
post.toc = getPageTableOfContents(post, post.blockMap)
if (post?.blockMap?.block) {
post.content = Object.keys(post.blockMap.block)
post.toc = getPageTableOfContents(post, post.blockMap)
}
const floatSlot = post?.toc?.length > 1
? <div className='block lg:hidden'><TocDrawerButton onClick={() => {
drawerRight?.current?.handleSwitchVisible()

View File

@@ -9,8 +9,8 @@ export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts
const { locale } = useGlobal()
const meta = {
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.title}`,
description: BLOG.description,
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}

View File

@@ -8,8 +8,8 @@ import TagItem from './components/TagItem'
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
description: BLOG.description,
title: `${locale.COMMON.TAGS} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase meta={meta} categories={categories} postCount={postCount} latestPosts={latestPosts}>

View File

@@ -1,9 +1,9 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG_NEXT from '../config_next'
export default function ArticleCopyright ({ author, url }) {
if (!BLOG.widget?.showCopyRight) {
if (!CONFIG_NEXT.ARTICLE_COPYRIGHT) {
return <></>
}
const { locale } = useGlobal()
@@ -11,7 +11,7 @@ export default function ArticleCopyright ({ author, url }) {
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-700 bg-gray-100 p-5 leading-8 border-l-2 border-blue-500">
<li>
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
<Link href="/about">
<Link href={'/about'} >
<a className="hover:underline">{author}</a>
</Link>
</li>

View File

@@ -1,9 +1,9 @@
import BLOG from '@/blog.config'
import BlogAround from '@/themes/NEXT/components/BlogAround'
import BlogAround from './BlogAround'
import Comment from '@/components/Comment'
import RecommendPosts from '@/themes/NEXT/components/RecommendPosts'
import ShareBar from '@/themes/NEXT/components/ShareBar'
import TagItem from '@/themes/NEXT/components/TagItem'
import RecommendPosts from './RecommendPosts'
import ShareBar from './ShareBar'
import TagItem from './TagItem'
import formatDate from '@/lib/formatDate'
import { useGlobal } from '@/lib/global'
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
@@ -13,7 +13,8 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-c'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
@@ -28,7 +29,7 @@ import WordCount from './WordCount'
* @returns
*/
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
const url = BLOG.link + useRouter().asPath
const url = BLOG.LINK + useRouter().asPath
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
@@ -102,8 +103,7 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
<div className="hidden busuanzi_container_page_pv font-light mr-2">
<FontAwesomeIcon icon={faEye} className='mr-1'/>
&nbsp;
<span className="mr-2 busuanzi_value_page_pv"
></span>
<span className="mr-2 busuanzi_value_page_pv"/>
<span className='mr-2'>|</span>
</div>
</div>
@@ -141,11 +141,11 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-2708419466378217"
data-ad-slot="3806269138"></ins>
data-ad-slot="3806269138"/>
</section>
{/* 版权声明 */}
<ArticleCopyright author={BLOG.author} url={url} />
<ArticleCopyright author={BLOG.AUTHOR} url={url} />
{/* 推荐文章 */}
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />

View File

@@ -12,15 +12,15 @@ export default function BlogAround ({ prev, next }) {
return <></>
}
return <section className='text-gray-800 border-t dark:text-gray-300 flex flex-wrap lg:flex-nowrap lg:space-x-10 justify-between py-2'>
<Link href={`/article/${prev.slug}`} passHref>
{prev && <Link href={`/article/${prev.slug}`} passHref>
<a className='text-sm py-3 text-gray-400 hover:underline cursor-pointer'>
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
</a>
</Link>
<Link href={`/article/${next.slug}`} passHref>
</Link>}
{next && <Link href={`/article/${next.slug}`} passHref>
<a className='text-sm flex py-3 text-gray-400 hover:underline cursor-pointer'>{next.title}
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
</a>
</Link>
</Link>}
</section>
}

View File

@@ -1,4 +1,4 @@
import React, { useRef } from 'react'
import React from 'react'
import Link from 'next/link'
import BLOG from '@/blog.config'
/**
@@ -9,17 +9,16 @@ import BLOG from '@/blog.config'
* @constructor
*/
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
const targetRef = useRef(null)
if (!posts || posts.length === 0) {
return <></>
} else {
return <div ref={targetRef}>
return <div>
<div className='pt-16 pb-4 text-3xl dark:text-gray-300' id={archiveTitle}>{archiveTitle}</div>
<ul>
{posts.map(post => (
<li key={post.id} className='border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500'>
<div name={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span> &nbsp;
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<div id={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span> &nbsp;
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className='dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600'>{post.title}</a>
</Link>
</div>

View File

@@ -8,16 +8,17 @@ import React from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import Card from './Card'
import TagItemMini from './TagItemMini'
import CONFIG_NEXT from '../config_next'
const BlogPostCard = ({ post, showSummary }) => {
const { locale } = useGlobal()
const showPreview = BLOG.home?.showPreview && post.blockMap
const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap
return (
<Card className='w-full'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
<div className='lg:p-8 p-4 flex flex-col w-full'>
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={`cursor-pointer font-bold hover:underline text-3xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
{post.title}
</a>
@@ -59,7 +60,7 @@ const BlogPostCard = ({ post, showSummary }) => {
</div> }
<div className='article-cover pointer-events-none'>
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className='hover:bg-opacity-100 hover:scale-105 pointer-events-auto transform duration-300 rounded-md p-2 text-red-500 cursor-pointer'>
{locale.COMMON.ARTICLE_DETAIL}
<FontAwesomeIcon className='ml-1' icon={faAngleRight} /></a>
@@ -67,8 +68,8 @@ const BlogPostCard = ({ post, showSummary }) => {
</div>
</div>
{BLOG.home?.showPostCover && post?.page_cover && (
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
{CONFIG_NEXT.POST_LIST_COVER && post?.page_cover && (
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<div className='h-72 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
</div>

View File

@@ -1,7 +1,7 @@
import BlogPostCard from '@/themes/NEXT/components/BlogPostCard'
import BlogPostCard from './BlogPostCard'
import PaginationNumber from './PaginationNumber'
import BLOG from '@/blog.config'
import BlogPostListEmpty from '@/themes/NEXT/components/BlogPostListEmpty'
import BlogPostListEmpty from './BlogPostListEmpty'
/**
* 文章列表分页表格
@@ -12,7 +12,7 @@ import BlogPostListEmpty from '@/themes/NEXT/components/BlogPostListEmpty'
* @constructor
*/
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />

View File

@@ -1,20 +1,20 @@
import BLOG from '@/blog.config'
import BlogPostCard from '@/themes/NEXT/components/BlogPostCard'
import BlogPostListEmpty from '@/themes/NEXT/components/BlogPostListEmpty'
import BlogPostCard from './BlogPostCard'
import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import CONFIG_NEXT from '../config_next'
/**
* 博客列表滚动分页
* @param posts 所有文章
* @param tags 所有标签
* @param targetRef 指向父容器,用于计算下拉滚动的高度
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home.showSummary }) => {
const postsPerPage = BLOG.postsPerPage
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_NEXT.POST_LIST_SUMMARY }) => {
const postsPerPage = BLOG.POSTS_PER_PAGE
const [page, updatePage] = useState(1)
const postsToShow = getPostByPage(page, posts, postsPerPage)

View File

@@ -1,6 +1,7 @@
import { loadUserThemeFromCookies, saveTheme, useGlobal } from '@/lib/global'
import { useGlobal } from '@/lib/global'
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
const DarkModeButton = () => {
const { changeTheme } = useGlobal()

View File

@@ -1,11 +1,11 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import CONFIG_NEXT from '../config_next'
export default function FloatDarkModeButton () {
if (!BLOG.widget?.showDarkMode) {
if (!CONFIG_NEXT.WIDGET_DARK_MODE) {
return <></>
}

View File

@@ -6,18 +6,17 @@ import BLOG from '@/blog.config'
const Footer = ({ title }) => {
const d = new Date()
const currentYear = d.getFullYear()
const startYear = BLOG.since && BLOG.since !== currentYear && BLOG.since + '-'
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
return (
<footer
className='dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-gray-400 text-sm p-6'
>
<FontAwesomeIcon icon={faCopyright} /> {`${startYear}${currentYear}`} <span><FontAwesomeIcon icon={faHeart} className='mx-1 animate-pulse'/> <a href={BLOG.link} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.author}</a>.
<FontAwesomeIcon icon={faCopyright} /> {`${startYear}${currentYear}`} <span><FontAwesomeIcon icon={faHeart} className='mx-1 animate-pulse'/> <a href={BLOG.LINK} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>.
<br/>
<span>Powered by <a href='https://notion.so' className='underline font-bold text-gray-500 dark:text-gray-300'>Notion</a> & <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span></span>
<br />
<FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2 font-bold'>闽ICP备20010331号</a>
{BLOG.BEI_AN && <><br /><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
<span className='hidden busuanzi_container_site_pv'>
<FontAwesomeIcon icon={faEye}/><span className='px-1 busuanzi_value_site_pv'> </span> </span>
<span className='pl-2 hidden busuanzi_container_site_uv'>

View File

@@ -1,9 +1,9 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useEffect, useState } from 'react'
import Typed from 'typed.js'
import CONFIG_NEXT from '../config_next'
let wrapperTop = 0
let windowTop = 0
@@ -19,7 +19,7 @@ export default function Header () {
if (!typed && window && document.getElementById('typed')) {
changeType(
new Typed('#typed', {
strings: BLOG.home.homeBannerStrings,
strings: CONFIG_NEXT.HOME_BANNER_Strings,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
@@ -41,7 +41,7 @@ export default function Header () {
const scrollTrigger = () => {
if (
(window.scrollY > windowTop) &
(window.scrollY < window.innerHeight) &
(window.scrollY < window.innerHeight) &&
!autoScroll
) {
autoScroll = true
@@ -50,7 +50,7 @@ export default function Header () {
}
if (
(window.scrollY < windowTop) &
(window.scrollY < window.innerHeight) &
(window.scrollY < window.innerHeight) &&
!autoScroll
) {
autoScroll = true
@@ -99,7 +99,7 @@ export default function Header () {
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black"
style={{
backgroundImage:
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${BLOG.home.homeBannerImage}")`
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${CONFIG_NEXT.HOME_BANNER_IMAGE}")`
}}
>
<div className="absolute flex h-full items-center lg:-mt-14 justify-center w-full text-4xl md:text-7xl text-white">

View File

@@ -4,12 +4,12 @@ import Router from 'next/router'
import React from 'react'
import SocialButton from './SocialButton'
const InfoCard = ({ postCount }) => {
const InfoCard = () => {
return <>
<div className='flex flex-col items-center justify-center '>
<div className='hover:rotate-45 hover:scale-125 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/about') }}>
<Image
alt={BLOG.author}
alt={BLOG.AUTHOR}
width={120}
height={120}
loading='lazy'
@@ -17,8 +17,8 @@ const InfoCard = ({ postCount }) => {
className='rounded-full'
/>
</div>
<div className='text-2xl font-serif dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.author}</div>
<div className='font-light dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.bio}</div>
<div className='text-2xl font-serif dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.AUTHOR}</div>
<div className='font-light dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.BIO}</div>
<SocialButton/>
</div>
</>

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useEffect, useState } from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import CONFIG_NEXT from '../config_next'
/**
* 跳转到网页顶部
@@ -13,7 +13,7 @@ import smoothscroll from 'smoothscroll-polyfill'
* @constructor
*/
const JumpToBottomButton = ({ showPercent = false }) => {
if (!BLOG.widget?.showToBottom) {
if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
return <></>
}

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import CONFIG_NEXT from '../config_next'
/**
* 跳转到网页顶部
@@ -13,7 +13,7 @@ import React from 'react'
* @constructor
*/
const JumpToTopButton = ({ showPercent = true, percent }) => {
if (!BLOG.widget?.showToTop) {
if (!CONFIG_NEXT.WIDGET_TO_TOP) {
return <></>
}
const { locale } = useGlobal()

View File

@@ -24,9 +24,9 @@ const LatestPostsGroup = ({ posts }) => {
<div className='font-light text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
</div>
{posts.map(post => {
const selected = currentPath === `${BLOG.path}/article/${post.slug}`
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
return (
<Link key={post.id} title={post.title} href={`${BLOG.path}/article/${post.slug}`} passHref>
<Link key={post.id} title={post.title} href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={ 'my-1 flex font-light'}>
<div className={ (selected ? 'text-white bg-gray-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-gray-500 px-2 duration-200 w-full ' +
'hover:text-white dark:hover:text-white cursor-pointer' }>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import throttle from 'lodash.throttle'
import DarkModeButton from '@/themes/NEXT/components/DarkModeButton'
import DarkModeButton from './DarkModeButton'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBars } from '@fortawesome/free-solid-svg-icons'

View File

@@ -1,9 +1,9 @@
/* eslint-disable no-undef */
import BLOG from '@/blog.config'
import CONFIG_NEXT from '../config_next'
let hasLoad = false
export default function Live2D () {
if (!BLOG.widget?.showPet) {
if (!CONFIG_NEXT.WIDGET_PET) {
return <></>
}
@@ -13,7 +13,7 @@ export default function Live2D () {
}
return <div className='fixed right-0 bottom-0 hidden md:block lg:mr-24 2xl:mr-40 z-20'>
<canvas id="live2d"className='animate__slideInLeft animate__animated' width="280" height="250"></canvas>
<canvas id="live2d" className='animate__slideInLeft animate__animated' width="280" height="250"/>
</div>
}
@@ -25,7 +25,7 @@ function initLive2D () {
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
]).then(() => {
// https://github.com/xiazeyu/live2d-widget-models
loadlive2d('live2d', BLOG.widget.petLink)
loadlive2d('live2d', CONFIG_NEXT.WIDGET_PET_LINK)
})
}
}

View File

@@ -5,8 +5,8 @@ import React from 'react'
const Logo = () => {
return <Link href='/' passHref>
<div className='flex flex-col justify-center items-center cursor-pointer bg-black space-y-3 h-32 font-bold'>
<div className='font-serif text-xl text-white'> {BLOG.title}</div>
<div className='text-sm text-gray-300 font-light'> {BLOG.description}</div>
<div className='font-serif text-xl text-white'> {BLOG.TITLE}</div>
<div className='text-sm text-gray-300 font-light'> {BLOG.DESCRIPTION}</div>
</div>
</Link>
}

View File

@@ -4,19 +4,19 @@ import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArchive, faHome, faTag, faTh, faUser } from '@fortawesome/free-solid-svg-icons'
import BLOG from 'blog.config'
import CONFIG_NEXT from '../config_next'
const MenuButtonGroup = ({ allowCollapse = false, postCount }) => {
const MenuButtonGroup = ({ postCount }) => {
const { locale } = useGlobal()
const router = useRouter()
const archiveSlot = <div className='bg-gray-300 dark:bg-gray-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
const links = [
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.menu.showCategory },
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.menu.showTag },
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: BLOG.menu.showArchive },
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: BLOG.menu.showAbout }
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_NEXT.MENU_CATEGORY },
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_NEXT.MENU_TAG },
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG_NEXT.MENU_ARCHIVE },
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_NEXT.MENU_ABOUT }
]
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans'>
{links.map(link => {

View File

@@ -1,120 +0,0 @@
import { useEffect, useRef } from 'react'
import Link from 'next/link'
import BLOG from '@/blog.config'
import Image from 'next/image'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBars } from '@fortawesome/free-solid-svg-icons'
const NavBar = () => {
const links = []
return (
<div className='flex-shrink-0'>
<ul className='flex flex-row'>
{links.map(
link =>
link.show && (
<li
key={link.id}
className='block ml-4 text-black dark:text-gray-50 nav'
>
<Link href={link.to}>
<a>{(link.icon && (<i className={'px-1 fa ' + link.icon} />))} {link.name}</a>
</Link>
</li>
)
)}
</ul>
</div>
)
}
const Header = ({ navBarTitle, fullWidth = true }) => {
const navRef = useRef(null)
const sentinelRef = useRef([])
// 当Header移出屏幕时改变的样式
const handler = ([entry]) => {
if (navRef && navRef.current) {
if (!entry.isIntersecting && entry !== undefined) {
navRef.current.classList.add('sticky-nav-full')
} else {
navRef.current.classList.remove('sticky-nav-full')
}
}
}
useEffect(() => {
const observer = new window.IntersectionObserver(handler)
observer.observe(sentinelRef.current)
// Don't touch this, I have no idea how it works XD
// return () => {
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
// }
}, [sentinelRef])
return (
<>
{BLOG.autoCollapsedNavBar === true && (
<script
dangerouslySetInnerHTML={{
__html: `
var windowTop=0;
function scrollTrigger(){
let scrollS = window.scrollY;
let nav = document.querySelector('.sticky-nav');
if(scrollS >= windowTop){
nav.style.opacity = 0;
windowTop = scrollS;
}else{
nav.style.opacity = 1;
windowTop = scrollS;
}
};
window.addEventListener('scroll',scrollTrigger);
`
}}
/>
)}
<div className='observer-element h-0.5 ' ref={sentinelRef} />
<div
className='z-30 px-4 sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center py-6 bg-white bg-opacity-80 '
id='sticky-nav'
ref={navRef}
>
<div className='flex items-center'>
<div className='flex cursor-pointer'>
<div className='px-2 text-xl'>
<FontAwesomeIcon icon={faBars} className='hover:scale-125 transform duration-200' />
</div>
<Image
alt={BLOG.title}
width={28}
height={28}
src='/avatar.jpg'
className='rounded-full'
/>
<div
className='mx-1 text-center cursor-pointer text-xl p-1
dark:bg-gray-900 dark:text-gray-300 font-semibold
dark:hover:bg-gray-600 text-black hover:scale-105
hover:shadow-2xl duration-200 transform'>{BLOG.title}</div>
</div>
{navBarTitle
? (
<p className='ml-1 font-medium text-gray-500 dark:text-night header-name'>
{navBarTitle}
</p>
)
: (
<p className='ml-1 font-medium dark:text-night header-name'>
<span className='font-normal'>{BLOG.description}</span>
</p>
)}
</div>
<NavBar />
</div>
</>
)
}
export default Header

View File

@@ -23,7 +23,7 @@ const PaginationNumber = ({ page, totalPage }) => {
{/* 上一页 */}
<Link
href={ {
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
pathname: (currentPage - 1 === 1 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
} } passHref >
<div
rel='prev'

View File

@@ -18,7 +18,7 @@ const PaginationSimple = ({ page, showNext }) => {
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
<Link
href={ {
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
pathname: (currentPage - 1 === 1 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
} } passHref >
<button
rel='prev'

Some files were not shown because too many files have changed in this diff Show More