nnbb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.local +2 -0
- package/.eslintrc.json +15 -0
- package/.github/stale.yml +16 -0
- package/.github/workflows/codeql-analysis.yml +73 -0
- package/.github/workflows/docker-ghcr.yaml +59 -0
- package/.prettierrc +9 -0
- package/.vscode/launch.json +28 -0
- package/.vscode/settings.json +6 -0
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/blog.config.js +454 -0
- package/components/Ackee.js +83 -0
- package/components/AdBlockDetect.js +40 -0
- package/components/AlgoliaSearchModal.js +250 -0
- package/components/Artalk.js +30 -0
- package/components/Busuanzi.js +26 -0
- package/components/ChatBase.js +19 -0
- package/components/Collapse.tsx +134 -0
- package/components/Comment.js +161 -0
- package/components/CommonHead.tsx +101 -0
- package/components/CommonScript.js +125 -0
- package/components/CusdisComponent.js +35 -0
- package/components/CustomContextMenu.js +221 -0
- package/components/DebugPanel.js +134 -0
- package/components/DisableCopy.js +21 -0
- package/components/Draggable.js +167 -0
- package/components/Equation.js +31 -0
- package/components/ExternalPlugins.js +75 -0
- package/components/ExternalScript.js +29 -0
- package/components/FacebookMessenger.js +255 -0
- package/components/FacebookPage.js +34 -0
- package/components/Fireworks.js +210 -0
- package/components/FlipCard.js +56 -0
- package/components/FlutteringRibbon.js +322 -0
- package/components/FullScreenButton.js +48 -0
- package/components/Giscus.js +33 -0
- package/components/Gitalk.js +42 -0
- package/components/GoogleAdsense.js +111 -0
- package/components/Gtag.js +18 -0
- package/components/HeroIcons.tsx +321 -0
- package/components/KatexReact.js +53 -0
- package/components/LazyImage.js +95 -0
- package/components/Live2D.js +52 -0
- package/components/Loading.js +20 -0
- package/components/Mark.js +28 -0
- package/components/NProgress.ts +8 -0
- package/components/Nest.js +124 -0
- package/components/NotionIcon.js +20 -0
- package/components/NotionPage.tsx +206 -0
- package/components/Player.js +54 -0
- package/components/PrismMac.js +236 -0
- package/components/QrCode.js +34 -0
- package/components/Ribbon.js +98 -0
- package/components/Sakura.js +192 -0
- package/components/Select.js +40 -0
- package/components/ShareBar.js +29 -0
- package/components/ShareButtons.js +403 -0
- package/components/SideBarDrawer.js +50 -0
- package/components/StarrySky.js +130 -0
- package/components/Tabs.js +64 -0
- package/components/ThemeSwitch.js +67 -0
- package/components/Twikoo.js +27 -0
- package/components/TwikooCommentCount.js +22 -0
- package/components/TwikooCommentCounter.js +78 -0
- package/components/TwikooRecentComments.js +11 -0
- package/components/Utterances.js +35 -0
- package/components/VConsole.js +76 -0
- package/components/ValineComponent.js +59 -0
- package/components/ValineCount.js +6 -0
- package/components/ValinePanel.js +3 -0
- package/components/Vercel.tsx +54 -0
- package/components/WWAds.js +18 -0
- package/components/WalineComponent.js +83 -0
- package/components/WebMention.js +173 -0
- package/components/Webwhiz.js +17 -0
- package/components/WordCount.js +73 -0
- package/hooks/useToggleClickOutSide.ts +32 -0
- package/hooks/useWindowSize.ts +30 -0
- package/lib/algolia.js +108 -0
- package/lib/busuanzi.js +99 -0
- package/lib/cache/cacheManager.ts +49 -0
- package/lib/cache/localFileCache.ts +56 -0
- package/lib/cache/memoryMache.ts +20 -0
- package/lib/cache/mongoDbCache.ts +70 -0
- package/lib/cache/types.ts +5 -0
- package/lib/font.js +46 -0
- package/lib/global.tsx +129 -0
- package/lib/gtag.js +17 -0
- package/lib/mailchimp.js +49 -0
- package/lib/memorize.js +0 -0
- package/lib/mhchem.js +1696 -0
- package/lib/notion/getAllCategories.ts +51 -0
- package/lib/notion/getAllPageIds.ts +51 -0
- package/lib/notion/getAllPosts.js +68 -0
- package/lib/notion/getAllTags.ts +43 -0
- package/lib/notion/getNotionData.ts +340 -0
- package/lib/notion/getPageInfoOfPostPage.ts +58 -0
- package/lib/notion/getPageProperties.ts +203 -0
- package/lib/notion/getPageTableOfContents.ts +107 -0
- package/lib/notion/getPostBlocks.ts +147 -0
- package/lib/notion/mapImage.ts +130 -0
- package/lib/notion/types.ts +125 -0
- package/lib/notion.js +2 -0
- package/lib/robots.txt.js +25 -0
- package/lib/rss.js +63 -0
- package/lib/sitemap.xml.js +67 -0
- package/lib/utils.js +212 -0
- package/next-env.d.ts +5 -0
- package/next-i18next.config.js +7 -0
- package/next-sitemap.config.js +11 -0
- package/next.config.js +124 -0
- package/package.json +92 -0
- package/pages/404.tsx +40 -0
- package/pages/[prefix]/[slug].tsx +123 -0
- package/pages/[prefix]/index.tsx +223 -0
- package/pages/_app.js +59 -0
- package/pages/_document.js +42 -0
- package/pages/api/subscribe.js +22 -0
- package/pages/archive/index.tsx +79 -0
- package/pages/category/[category]/index.tsx +87 -0
- package/pages/category/[category]/page/[page].tsx +103 -0
- package/pages/category/index.tsx +43 -0
- package/pages/index.tsx +88 -0
- package/pages/page/[page].tsx +93 -0
- package/pages/search/[keyword]/index.tsx +162 -0
- package/pages/search/[keyword]/page/[page].tsx +166 -0
- package/pages/search/index.tsx +69 -0
- package/pages/sitemap.xml.js +70 -0
- package/pages/tag/[tag]/index.tsx +73 -0
- package/pages/tag/[tag]/page/[page].tsx +87 -0
- package/pages/tag/index.tsx +42 -0
- package/postcss.config.js +6 -0
- package/public/ads.txt +1 -0
- package/public/avatar.png +0 -0
- package/public/avatar.svg +11 -0
- package/public/bg_image.jpg +0 -0
- package/public/css/all.min.css +9 -0
- package/public/css/custom.css +8 -0
- package/public/css/img-shadow.css +5 -0
- package/public/css/prism-mac-style.css +58 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/js/cusdis.es.js +107 -0
- package/public/js/custom.js +1 -0
- package/public/locales/en/common.json +44 -0
- package/public/locales/en/menu.json +9 -0
- package/public/locales/en/nav.json +11 -0
- package/public/locales/zh-CN/common.json +44 -0
- package/public/locales/zh-CN/menu.json +9 -0
- package/public/locales/zh-CN/nav.json +9 -0
- package/public/webfonts/fa-brands-400.ttf +0 -0
- package/public/webfonts/fa-brands-400.woff2 +0 -0
- package/public/webfonts/fa-regular-400.ttf +0 -0
- package/public/webfonts/fa-regular-400.woff2 +0 -0
- package/public/webfonts/fa-solid-900.ttf +0 -0
- package/public/webfonts/fa-solid-900.woff2 +0 -0
- package/public/webfonts/fa-v4compatibility.ttf +0 -0
- package/public/webfonts/fa-v4compatibility.woff2 +0 -0
- package/styles/animate.css +503 -0
- package/styles/globals.css +183 -0
- package/styles/notion.css +2064 -0
- package/styles/nprogress.css +84 -0
- package/styles/prism-theme.css +119 -0
- package/styles/utility-patterns.css +79 -0
- package/tailwind.config.js +37 -0
- package/theme/index.ts +6 -0
- package/theme/types/@theme-components.d.ts +29 -0
- package/theme/useLayout.ts +41 -0
- package/theme/utils.ts +108 -0
- package/themes/innocent/package.json +7 -0
- package/themes/innocent/theme.config.js +1 -0
- package/themes/nobelium/components/Announcement.tsx +27 -0
- package/themes/nobelium/components/ArticleFooter.tsx +39 -0
- package/themes/nobelium/components/ArticleInfo.tsx +58 -0
- package/themes/nobelium/components/ArticleLock.tsx +86 -0
- package/themes/nobelium/components/BlogArchiveItem.js +41 -0
- package/themes/nobelium/components/BlogListBar.js +39 -0
- package/themes/nobelium/components/BlogListPage.tsx +67 -0
- package/themes/nobelium/components/BlogListScroll.tsx +96 -0
- package/themes/nobelium/components/BlogPost.tsx +33 -0
- package/themes/nobelium/components/DarkModeButton.tsx +50 -0
- package/themes/nobelium/components/ExampleRecentComments.js +35 -0
- package/themes/nobelium/components/Footer.tsx +35 -0
- package/themes/nobelium/components/JumpToTopButton.tsx +39 -0
- package/themes/nobelium/components/LanguageSwitchButton.tsx +58 -0
- package/themes/nobelium/components/MenuItemCollapse.tsx +92 -0
- package/themes/nobelium/components/MenuItemDrop.tsx +83 -0
- package/themes/nobelium/components/Nav/Nav.module.css +50 -0
- package/themes/nobelium/components/Nav/Nav.tsx +187 -0
- package/themes/nobelium/components/RandomPostButton.tsx +31 -0
- package/themes/nobelium/components/SearchButton.tsx +31 -0
- package/themes/nobelium/components/SearchInput.tsx +94 -0
- package/themes/nobelium/components/SearchNavBar.js +19 -0
- package/themes/nobelium/components/SideBar.js +83 -0
- package/themes/nobelium/components/SvgIcon.js +29 -0
- package/themes/nobelium/components/TagItem.js +13 -0
- package/themes/nobelium/components/Tags.tsx +44 -0
- package/themes/nobelium/components/Title.js +19 -0
- package/themes/nobelium/index.tsx +28 -0
- package/themes/nobelium/layout/LayoutBase.tsx +79 -0
- package/themes/nobelium/pages/Archive.tsx +30 -0
- package/themes/nobelium/pages/Category.tsx +43 -0
- package/themes/nobelium/pages/Home.tsx +22 -0
- package/themes/nobelium/pages/PageNotFound.tsx +15 -0
- package/themes/nobelium/pages/Post.tsx +34 -0
- package/themes/nobelium/pages/PostList.tsx +74 -0
- package/themes/nobelium/pages/Search.tsx +65 -0
- package/themes/nobelium/pages/Tag.tsx +42 -0
- package/themes/nobelium/providers/index.tsx +60 -0
- package/themes/nobelium/stores/index.tsx +42 -0
- package/themes/nobelium/theme.config.ts +17 -0
- package/themes/nobelium/types/index.ts +10 -0
- package/tsconfig.json +29 -0
- package/types/index.ts +1 -0
- package/types/page.ts +102 -0
- package/vercel.json +5 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
2
|
+
import { useTranslation } from 'next-i18next';
|
3
|
+
|
4
|
+
import type { FC } from 'react';
|
5
|
+
|
6
|
+
export interface ArticleInfoProps {
|
7
|
+
validPassword: (password: string) => boolean;
|
8
|
+
}
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 加密文章校验组件
|
12
|
+
* @param {password, validPassword} props
|
13
|
+
* @param password 正确的密码
|
14
|
+
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
15
|
+
* @returns
|
16
|
+
*/
|
17
|
+
const ArticleLock: FC<ArticleInfoProps> = (props) => {
|
18
|
+
const { validPassword } = props;
|
19
|
+
const { t } = useTranslation('common');
|
20
|
+
const [password, setPassword] = useState('');
|
21
|
+
const [isShowTip, setIsShowTip] = useState(false);
|
22
|
+
const passwordInputRef = useRef<HTMLInputElement>(null);
|
23
|
+
|
24
|
+
const submitPassword = useCallback(() => {
|
25
|
+
if (!validPassword(password)) {
|
26
|
+
setIsShowTip(true);
|
27
|
+
} else {
|
28
|
+
setIsShowTip(false);
|
29
|
+
}
|
30
|
+
}, [password, validPassword]);
|
31
|
+
|
32
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
33
|
+
if (e.key === 'Enter') {
|
34
|
+
submitPassword();
|
35
|
+
}
|
36
|
+
};
|
37
|
+
|
38
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
39
|
+
setPassword(e.target.value);
|
40
|
+
|
41
|
+
useEffect(() => {
|
42
|
+
// 选中密码输入框并将其聚焦
|
43
|
+
passwordInputRef.current?.focus();
|
44
|
+
}, []);
|
45
|
+
|
46
|
+
return (
|
47
|
+
<div
|
48
|
+
id="container"
|
49
|
+
className="flex h-96 w-full items-center justify-center font-sans"
|
50
|
+
>
|
51
|
+
<div className="space-y-3 text-center">
|
52
|
+
<div className="font-bold">{t('article-lock-tips')}</div>
|
53
|
+
<div className="flex">
|
54
|
+
<input
|
55
|
+
id="password"
|
56
|
+
type="password"
|
57
|
+
value={password}
|
58
|
+
onKeyDown={handleKeyDown}
|
59
|
+
onChange={handleChange}
|
60
|
+
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
|
61
|
+
className="w-full rounded-l bg-gray-50 pl-5 text-sm font-light leading-10 text-black outline-none transition focus:shadow-lg dark:bg-gray-500"
|
62
|
+
></input>
|
63
|
+
<div
|
64
|
+
onClick={submitPassword}
|
65
|
+
className="cursor-pointer items-center justify-center whitespace-nowrap rounded-r bg-gray-300 px-3 py-2 duration-300"
|
66
|
+
>
|
67
|
+
<i
|
68
|
+
className={
|
69
|
+
'fas fa-key cursor-pointer duration-200 dark:text-black'
|
70
|
+
}
|
71
|
+
>
|
72
|
+
{t('submit')}
|
73
|
+
</i>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
{isShowTip && (
|
77
|
+
<div className="animate__shakeX animate__animated text-red-500">
|
78
|
+
${t('password-error')}
|
79
|
+
</div>
|
80
|
+
)}
|
81
|
+
</div>
|
82
|
+
</div>
|
83
|
+
);
|
84
|
+
};
|
85
|
+
|
86
|
+
export default ArticleLock;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import BLOG from '@/blog.config'
|
2
|
+
import Link from 'next/link'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* 归档分组文章
|
6
|
+
* @param {*} param0
|
7
|
+
* @returns
|
8
|
+
*/
|
9
|
+
export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
|
10
|
+
return (
|
11
|
+
<div key={archiveTitle}>
|
12
|
+
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
|
13
|
+
{archiveTitle}
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<ul>
|
17
|
+
{archivePosts[archiveTitle].map(post => (
|
18
|
+
<li
|
19
|
+
key={post.id}
|
20
|
+
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"
|
21
|
+
>
|
22
|
+
<div id={post?.publishDay}>
|
23
|
+
<span className="text-gray-400">
|
24
|
+
{post.date?.start_date}
|
25
|
+
</span>{' '}
|
26
|
+
|
27
|
+
<Link
|
28
|
+
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
29
|
+
passHref
|
30
|
+
className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
31
|
+
|
32
|
+
{post.title}
|
33
|
+
|
34
|
+
</Link>
|
35
|
+
</div>
|
36
|
+
</li>
|
37
|
+
))}
|
38
|
+
</ul>
|
39
|
+
</div>
|
40
|
+
)
|
41
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import Tags from './Tags';
|
2
|
+
|
3
|
+
export default function BlogListBar(props) {
|
4
|
+
const { tag, setFilterKey } = props;
|
5
|
+
const handleSearchChange = (val) => {
|
6
|
+
setFilterKey(val);
|
7
|
+
};
|
8
|
+
if (tag) {
|
9
|
+
return (
|
10
|
+
<div className="mb-4">
|
11
|
+
<div className="relative">
|
12
|
+
<input
|
13
|
+
type="text"
|
14
|
+
placeholder={tag ? `Search in #${tag}` : 'Search Articles'}
|
15
|
+
className="block w-full border border-black bg-white px-4 py-2 text-black outline-none dark:border-white dark:bg-night dark:text-white"
|
16
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
17
|
+
/>
|
18
|
+
<svg
|
19
|
+
className="absolute right-3 top-3 h-5 w-5 text-black dark:text-white"
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
21
|
+
fill="none"
|
22
|
+
viewBox="0 0 24 24"
|
23
|
+
stroke="currentColor"
|
24
|
+
>
|
25
|
+
<path
|
26
|
+
strokeLinecap="round"
|
27
|
+
strokeLinejoin="round"
|
28
|
+
strokeWidth="2"
|
29
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
30
|
+
></path>
|
31
|
+
</svg>
|
32
|
+
</div>
|
33
|
+
<Tags {...props} />
|
34
|
+
</div>
|
35
|
+
);
|
36
|
+
} else {
|
37
|
+
return <></>;
|
38
|
+
}
|
39
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import { useRouter } from 'next/router';
|
3
|
+
import Link from 'next/link';
|
4
|
+
import BlogPost from './BlogPost';
|
5
|
+
import { useTranslation } from 'next-i18next';
|
6
|
+
|
7
|
+
import type { FC } from 'react';
|
8
|
+
import { PageInfo } from '@/lib/notion/types';
|
9
|
+
|
10
|
+
export interface BlogListPageProps {
|
11
|
+
posts: PageInfo[];
|
12
|
+
page?: number;
|
13
|
+
postCount: number;
|
14
|
+
}
|
15
|
+
|
16
|
+
const BlogListPage: FC<BlogListPageProps> = (props) => {
|
17
|
+
const { t } = useTranslation('common');
|
18
|
+
const { page = 1, posts, postCount } = props;
|
19
|
+
const router = useRouter();
|
20
|
+
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE);
|
21
|
+
const currentPage = +page;
|
22
|
+
|
23
|
+
const showPrev = currentPage > 1;
|
24
|
+
const showNext = currentPage < totalPage && posts?.length > 0;
|
25
|
+
const pagePrefix = router.asPath
|
26
|
+
.split('?')[0]
|
27
|
+
.replace(/\/page\/[1-9]\d*/, '')
|
28
|
+
.replace(/\/$/, '');
|
29
|
+
|
30
|
+
return (
|
31
|
+
<div className="my-6 w-full">
|
32
|
+
<div id="posts-wrapper">
|
33
|
+
{posts?.map((post) => <BlogPost key={post.id} post={post} />)}
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div className="flex justify-between text-xs">
|
37
|
+
<Link
|
38
|
+
href={{
|
39
|
+
pathname:
|
40
|
+
currentPage - 1 === 1
|
41
|
+
? `${pagePrefix}/`
|
42
|
+
: `${pagePrefix}/page/${currentPage - 1}`,
|
43
|
+
query: router.query.s ? { s: router.query.s } : {},
|
44
|
+
}}
|
45
|
+
className={`${showPrev ? ' ' : ' pointer-events-none invisible block '}no-underline rounded px-3 py-2`}
|
46
|
+
>
|
47
|
+
<button rel="prev" className="block cursor-pointer">
|
48
|
+
{t('prev')}
|
49
|
+
</button>
|
50
|
+
</Link>
|
51
|
+
<Link
|
52
|
+
href={{
|
53
|
+
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
54
|
+
query: router.query.s ? { s: router.query.s } : {},
|
55
|
+
}}
|
56
|
+
className={`${showNext ? ' ' : 'pointer-events-none invisible '} rounded px-3 py-2 no-underline`}
|
57
|
+
>
|
58
|
+
<button rel="next" className="block cursor-pointer">
|
59
|
+
{t('next')}
|
60
|
+
</button>
|
61
|
+
</Link>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
);
|
65
|
+
};
|
66
|
+
|
67
|
+
export default BlogListPage;
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
2
|
+
import BLOG from '@/blog.config';
|
3
|
+
import Link from 'next/link';
|
4
|
+
import React from 'react';
|
5
|
+
import { throttle } from 'lodash';
|
6
|
+
import { useTranslation } from 'next-i18next';
|
7
|
+
import dayjs from 'dayjs';
|
8
|
+
|
9
|
+
import type { FC } from 'react';
|
10
|
+
import { PageInfo } from '@/lib/notion/types';
|
11
|
+
|
12
|
+
export interface BlogListScrollProps {
|
13
|
+
posts: PageInfo[];
|
14
|
+
}
|
15
|
+
|
16
|
+
const BlogListScroll: FC<BlogListScrollProps> = (props) => {
|
17
|
+
const { posts } = props;
|
18
|
+
const { t } = useTranslation('common');
|
19
|
+
const [page, setPage] = useState(1);
|
20
|
+
const targetRef = useRef<HTMLDivElement>(null);
|
21
|
+
|
22
|
+
const hasMore = useMemo(
|
23
|
+
() => page * BLOG.POSTS_PER_PAGE < posts.length,
|
24
|
+
[posts, page],
|
25
|
+
);
|
26
|
+
|
27
|
+
const postsToShow = useMemo(
|
28
|
+
() => posts.slice(0, BLOG.POSTS_PER_PAGE * page),
|
29
|
+
[posts, page],
|
30
|
+
);
|
31
|
+
|
32
|
+
const handleGetMore = useCallback(() => {
|
33
|
+
if (hasMore) {
|
34
|
+
setPage((prevPage) => prevPage + 1);
|
35
|
+
}
|
36
|
+
}, [hasMore]);
|
37
|
+
|
38
|
+
// 监听滚动自动分页加载
|
39
|
+
const scrollTrigger = useCallback(() => {
|
40
|
+
const scrollPosition = window.scrollY + window.innerHeight;
|
41
|
+
const clientHeight = targetRef.current?.clientHeight || 0;
|
42
|
+
if (scrollPosition > clientHeight + 100) {
|
43
|
+
handleGetMore();
|
44
|
+
}
|
45
|
+
}, [handleGetMore]);
|
46
|
+
|
47
|
+
useEffect(() => {
|
48
|
+
const scrollTriggerThrottled = throttle(scrollTrigger, 500);
|
49
|
+
window.addEventListener('scroll', scrollTriggerThrottled);
|
50
|
+
return () => {
|
51
|
+
window.removeEventListener('scroll', scrollTriggerThrottled);
|
52
|
+
};
|
53
|
+
}, [scrollTrigger]);
|
54
|
+
|
55
|
+
return (
|
56
|
+
<div id="posts-wrapper" className="mb-12 w-full md:pr-12" ref={targetRef}>
|
57
|
+
{postsToShow.map((post) => (
|
58
|
+
<article key={post.id} className="mb-12">
|
59
|
+
<h2 className="mb-4">
|
60
|
+
<Link
|
61
|
+
href={`/${post.slug}`}
|
62
|
+
className="text-xl text-black no-underline hover:underline md:text-2xl"
|
63
|
+
>
|
64
|
+
{post.title}
|
65
|
+
</Link>
|
66
|
+
</h2>
|
67
|
+
|
68
|
+
<div className="mb-4 text-sm text-gray-700">
|
69
|
+
by
|
70
|
+
<a href="#" className="text-gray-700">
|
71
|
+
{BLOG.AUTHOR}
|
72
|
+
</a>
|
73
|
+
on {dayjs(post.publishDate).format('YYYY-MM-DD')}
|
74
|
+
<span className="mx-1 font-bold"> | </span>
|
75
|
+
<a href="#" className="text-gray-700">
|
76
|
+
{post.category}
|
77
|
+
</a>
|
78
|
+
<span className="mx-1 font-bold"> | </span>
|
79
|
+
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<p className="leading-normal text-gray-700">{post.summary}</p>
|
83
|
+
</article>
|
84
|
+
))}
|
85
|
+
|
86
|
+
<div
|
87
|
+
onClick={handleGetMore}
|
88
|
+
className="my-4 w-full cursor-pointer py-4 text-center "
|
89
|
+
>
|
90
|
+
{hasMore ? t('more') : `${t('no-more')} 😰`}{' '}
|
91
|
+
</div>
|
92
|
+
</div>
|
93
|
+
);
|
94
|
+
};
|
95
|
+
|
96
|
+
export default BlogListScroll;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import Link from 'next/link';
|
2
|
+
import BLOG from '@/blog.config';
|
3
|
+
import dayjs from 'dayjs';
|
4
|
+
|
5
|
+
import type { FC } from 'react';
|
6
|
+
import type { PageInfo } from '@/lib/notion/types';
|
7
|
+
|
8
|
+
export interface BlogPostProps {
|
9
|
+
post: PageInfo;
|
10
|
+
}
|
11
|
+
const BlogPost: FC<BlogPostProps> = ({ post }) => {
|
12
|
+
return (
|
13
|
+
<Link href={`${BLOG.SUB_PATH}/${post.slug}`}>
|
14
|
+
<article key={post.id} className="mb-6 md:mb-8">
|
15
|
+
<header className="flex flex-col justify-between md:flex-row md:items-baseline">
|
16
|
+
<div className="mb-2 mr-4 cursor-pointer text-lg font-medium text-black dark:text-gray-100 md:text-xl">
|
17
|
+
{post.title}
|
18
|
+
</div>
|
19
|
+
<time className="flex-shrink-0 text-gray-600 dark:text-gray-400">
|
20
|
+
{dayjs(post?.publishDate).format('YYYY-MM-DD')}
|
21
|
+
</time>
|
22
|
+
</header>
|
23
|
+
<main>
|
24
|
+
<p className="hidden leading-8 text-gray-700 dark:text-gray-300 md:block">
|
25
|
+
{post.summary}
|
26
|
+
</p>
|
27
|
+
</main>
|
28
|
+
</article>
|
29
|
+
</Link>
|
30
|
+
);
|
31
|
+
};
|
32
|
+
|
33
|
+
export default BlogPost;
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { useGlobal } from '@/lib/global';
|
2
|
+
import { Moon, Sun } from '@/components/HeroIcons';
|
3
|
+
import { useImperativeHandle } from 'react';
|
4
|
+
|
5
|
+
import type { FC, RefObject } from 'react';
|
6
|
+
|
7
|
+
export interface DarkModeButtonProps {
|
8
|
+
className?: string;
|
9
|
+
cRef?: RefObject<{ handleChangeDarkMode: () => void }>;
|
10
|
+
}
|
11
|
+
|
12
|
+
/**
|
13
|
+
* 深色模式按钮
|
14
|
+
*/
|
15
|
+
const DarkModeButton: FC<DarkModeButtonProps> = (props) => {
|
16
|
+
const { cRef, className } = props;
|
17
|
+
const { isDarkMode, updateDarkMode } = useGlobal();
|
18
|
+
|
19
|
+
/**
|
20
|
+
* 对外暴露方法
|
21
|
+
*/
|
22
|
+
useImperativeHandle(cRef, () => {
|
23
|
+
return {
|
24
|
+
handleChangeDarkMode: () => {
|
25
|
+
handleChangeDarkMode();
|
26
|
+
},
|
27
|
+
};
|
28
|
+
});
|
29
|
+
|
30
|
+
// 用户手动设置主题
|
31
|
+
const handleChangeDarkMode = () => {
|
32
|
+
const newStatus = !isDarkMode;
|
33
|
+
updateDarkMode(newStatus);
|
34
|
+
};
|
35
|
+
|
36
|
+
return (
|
37
|
+
<div
|
38
|
+
onClick={handleChangeDarkMode}
|
39
|
+
className={`${
|
40
|
+
className || ''
|
41
|
+
} flex h-10 w-10 items-center justify-center rounded-full text-gray-800 hover:bg-black hover:bg-opacity-10 dark:text-gray-200 dark:hover:bg-white dark:hover:bg-opacity-10`}
|
42
|
+
>
|
43
|
+
<div id="darkModeButton" className=" h-5 w-5 transform cursor-pointer ">
|
44
|
+
{isDarkMode ? <Sun /> : <Moon />}
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
);
|
48
|
+
};
|
49
|
+
|
50
|
+
export default DarkModeButton;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import BLOG from '@/blog.config'
|
3
|
+
import Link from 'next/link'
|
4
|
+
import { RecentComments } from '@waline/client'
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @see https://waline.js.org/guide/get-started.html
|
8
|
+
* @param {*} props
|
9
|
+
* @returns
|
10
|
+
*/
|
11
|
+
const ExampleRecentComments = (props) => {
|
12
|
+
const [comments, updateComments] = React.useState([])
|
13
|
+
const [onLoading, changeLoading] = React.useState(true)
|
14
|
+
React.useEffect(() => {
|
15
|
+
RecentComments({
|
16
|
+
serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
|
17
|
+
count: 5
|
18
|
+
}).then(({ comments }) => {
|
19
|
+
changeLoading(false)
|
20
|
+
updateComments(comments)
|
21
|
+
})
|
22
|
+
}, [])
|
23
|
+
|
24
|
+
return <>
|
25
|
+
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
|
26
|
+
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
|
27
|
+
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
|
28
|
+
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
|
29
|
+
<div className='dark:text-gray-400 text-gray-400 font-sans text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</Link></div>
|
30
|
+
</div>)}
|
31
|
+
|
32
|
+
</>
|
33
|
+
}
|
34
|
+
|
35
|
+
export default ExampleRecentComments
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
// import DarkModeButton from '@/components/DarkModeButton'
|
3
|
+
import Vercel from '@/components/Vercel';
|
4
|
+
|
5
|
+
const Footer = () => {
|
6
|
+
const d = new Date();
|
7
|
+
const currentYear = d.getFullYear();
|
8
|
+
|
9
|
+
const copyrightDate = (function () {
|
10
|
+
if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
|
11
|
+
return BLOG.SINCE + '-' + currentYear;
|
12
|
+
}
|
13
|
+
return currentYear;
|
14
|
+
})();
|
15
|
+
|
16
|
+
return (
|
17
|
+
<footer className="relative z-10 m-auto mt-6 w-full max-w-2xl flex-shrink-0 px-4 text-gray-500 transition-all dark:text-gray-400">
|
18
|
+
{/* <DarkModeButton className="text-center py-4" /> */}
|
19
|
+
<hr className="border-gray-200 dark:border-gray-600" />
|
20
|
+
<div className="my-4 text-sm leading-6">
|
21
|
+
<div className="flex flex-col items-center md:flex-row">
|
22
|
+
<div className="mr-0 md:mr-2">
|
23
|
+
© {BLOG.AUTHOR} {copyrightDate}.
|
24
|
+
</div>
|
25
|
+
<div className="mb-2 mr-0 md:mb-0 md:mr-auto">
|
26
|
+
Licensed under <a href={BLOG.LICENSE_URL}>{BLOG.LICENSE}</a>.
|
27
|
+
</div>
|
28
|
+
<Vercel />
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
</footer>
|
32
|
+
);
|
33
|
+
};
|
34
|
+
|
35
|
+
export default Footer;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { useEffect, useState } from 'react';
|
2
|
+
import { useTranslation } from 'next-i18next';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* 跳转到网页顶部
|
6
|
+
* 当屏幕下滑500像素后会出现该控件
|
7
|
+
* @param targetRef 关联高度的目标html标签
|
8
|
+
* @param showPercent 是否显示百分比
|
9
|
+
* @returns {JSX.Element}
|
10
|
+
* @constructor
|
11
|
+
*/
|
12
|
+
const JumpToTopButton = () => {
|
13
|
+
const { t } = useTranslation('common');
|
14
|
+
const [isShowButton, setIsShowButton] = useState(false);
|
15
|
+
|
16
|
+
useEffect(() => {
|
17
|
+
const scrollHandler = () => {
|
18
|
+
if (window.scrollY > 500) {
|
19
|
+
setIsShowButton(true);
|
20
|
+
} else {
|
21
|
+
setIsShowButton(false);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
window.addEventListener('scroll', scrollHandler);
|
25
|
+
return () => window.removeEventListener('scroll', scrollHandler);
|
26
|
+
}, []);
|
27
|
+
|
28
|
+
return (
|
29
|
+
<div
|
30
|
+
title={t('top')}
|
31
|
+
className={`cursor-pointer transition-all ${isShowButton ? 'opacity-100' : 'opacity-0'}`}
|
32
|
+
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
33
|
+
>
|
34
|
+
<i className="fas fa-angle-up text-2xl" />
|
35
|
+
</div>
|
36
|
+
);
|
37
|
+
};
|
38
|
+
|
39
|
+
export default JumpToTopButton;
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { useRouter } from 'next/router';
|
2
|
+
import Collapse from '@/components/Collapse';
|
3
|
+
import { useRef, useState } from 'react';
|
4
|
+
import { Language } from '@/components/HeroIcons';
|
5
|
+
import useToggleClickOutSide from '@/hooks/useToggleClickOutSide';
|
6
|
+
import nextI18NextConfig from '@/next-i18next.config.js';
|
7
|
+
|
8
|
+
import type { RefObject } from 'react';
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 语言切换
|
12
|
+
*/
|
13
|
+
export default function LanguageSwitchButton() {
|
14
|
+
const [isOpen, setIsOpen] = useState(false);
|
15
|
+
const menuRef = useRef<HTMLElement>(null) as RefObject<HTMLDivElement>;
|
16
|
+
const router = useRouter();
|
17
|
+
const { locales } = nextI18NextConfig.i18n;
|
18
|
+
|
19
|
+
useToggleClickOutSide(menuRef, () => {
|
20
|
+
setIsOpen(false);
|
21
|
+
});
|
22
|
+
|
23
|
+
const changeLanguage = (locale: string) => {
|
24
|
+
router.push(
|
25
|
+
{
|
26
|
+
pathname: router.pathname,
|
27
|
+
query: router.query,
|
28
|
+
},
|
29
|
+
router.asPath,
|
30
|
+
{ locale },
|
31
|
+
);
|
32
|
+
};
|
33
|
+
|
34
|
+
return (
|
35
|
+
<div className="relative" onClick={() => setIsOpen(!isOpen)} ref={menuRef}>
|
36
|
+
<div
|
37
|
+
className={`flex h-10 w-10 transform cursor-pointer items-center justify-center rounded-full text-gray-800 hover:bg-black hover:bg-opacity-10 dark:text-gray-200 dark:hover:bg-white dark:hover:bg-opacity-10`}
|
38
|
+
>
|
39
|
+
<Language className="h-5 w-5" />
|
40
|
+
</div>
|
41
|
+
<div
|
42
|
+
className={`${isOpen ? 'top-10 block opacity-100 ' : 'none top-8 opacity-0 '} absolute w-40 rounded border border-gray-100 bg-white drop-shadow-lg transition-all duration-300 dark:border-gray-800 dark:bg-black`}
|
43
|
+
>
|
44
|
+
<Collapse isOpen={isOpen}>
|
45
|
+
{locales.map((locale) => (
|
46
|
+
<div
|
47
|
+
className="p-3 text-gray-700 transition-all duration-200 hover:bg-gray-50 dark:border-gray-800 dark:text-gray-200 dark:hover:bg-gray-900"
|
48
|
+
key={locale}
|
49
|
+
onClick={() => changeLanguage(locale)}
|
50
|
+
>
|
51
|
+
{locale}
|
52
|
+
</div>
|
53
|
+
))}
|
54
|
+
</Collapse>
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
);
|
58
|
+
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import Collapse from '@/components/Collapse';
|
2
|
+
import Link from 'next/link';
|
3
|
+
import { useState } from 'react';
|
4
|
+
|
5
|
+
import type { FC } from 'react';
|
6
|
+
import type { NavLink } from '../types';
|
7
|
+
|
8
|
+
export interface MenuItemCollapseProps {
|
9
|
+
link: NavLink;
|
10
|
+
onHeightChange?: (params: { height: number }) => void;
|
11
|
+
}
|
12
|
+
|
13
|
+
/**
|
14
|
+
* 折叠菜单
|
15
|
+
* @param {*} param0
|
16
|
+
* @returns
|
17
|
+
*/
|
18
|
+
export const MenuItemCollapse: FC<MenuItemCollapseProps> = (props) => {
|
19
|
+
const { link } = props;
|
20
|
+
const [isOpen, changeIsOpen] = useState(false);
|
21
|
+
|
22
|
+
const hasSubMenu = link?.subMenus?.length && link?.subMenus?.length > 0;
|
23
|
+
|
24
|
+
const toggleOpenSubMenu = () => {
|
25
|
+
changeIsOpen(!isOpen);
|
26
|
+
};
|
27
|
+
|
28
|
+
if (!link || !link.show) {
|
29
|
+
return null;
|
30
|
+
}
|
31
|
+
|
32
|
+
return (
|
33
|
+
<div className="mb-2 transition-all duration-200 last:mb-0 dark:border-black dark:bg-hexo-black-gray">
|
34
|
+
{!hasSubMenu && (
|
35
|
+
<Link
|
36
|
+
href={link?.to}
|
37
|
+
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
38
|
+
className="flex px-4 dark:text-gray-200 "
|
39
|
+
>
|
40
|
+
{link?.icon && (
|
41
|
+
<div className="mr-2">
|
42
|
+
<i className={link.icon} />
|
43
|
+
</div>
|
44
|
+
)}
|
45
|
+
<div>{link?.name}</div>
|
46
|
+
</Link>
|
47
|
+
)}
|
48
|
+
{hasSubMenu && (
|
49
|
+
<div
|
50
|
+
onClick={hasSubMenu ? toggleOpenSubMenu : undefined}
|
51
|
+
className="flex cursor-pointer justify-center px-4 dark:text-gray-200 "
|
52
|
+
>
|
53
|
+
{link?.icon && (
|
54
|
+
<div className="mr-2">
|
55
|
+
<i className={link.icon} />
|
56
|
+
</div>
|
57
|
+
)}
|
58
|
+
<div className="mr-auto">{link?.name}</div>
|
59
|
+
<div className="flex items-center">
|
60
|
+
{isOpen ? (
|
61
|
+
<i className="fa fa-minus" />
|
62
|
+
) : (
|
63
|
+
<i className="fa fa-plus" />
|
64
|
+
)}
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
)}
|
68
|
+
{/* 折叠子菜单 */}
|
69
|
+
{hasSubMenu && (
|
70
|
+
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
71
|
+
<div className="flex flex-col">
|
72
|
+
{link.subMenus &&
|
73
|
+
link.subMenus.map((sLink) => {
|
74
|
+
return (
|
75
|
+
<Link
|
76
|
+
key={sLink.id}
|
77
|
+
href={sLink.to}
|
78
|
+
target={
|
79
|
+
link?.to?.indexOf('http') === 0 ? '_blank' : '_self'
|
80
|
+
}
|
81
|
+
className="mt-2 px-12"
|
82
|
+
>
|
83
|
+
{sLink.title}
|
84
|
+
</Link>
|
85
|
+
);
|
86
|
+
})}
|
87
|
+
</div>
|
88
|
+
</Collapse>
|
89
|
+
)}
|
90
|
+
</div>
|
91
|
+
);
|
92
|
+
};
|