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,83 @@
|
|
1
|
+
import Link from 'next/link';
|
2
|
+
import { useState } from 'react';
|
3
|
+
import useToggleClickOutSide from '@/hooks/useToggleClickOutSide';
|
4
|
+
import { useTranslation } from 'next-i18next';
|
5
|
+
import { useRef } from 'react';
|
6
|
+
|
7
|
+
import type { FC } from 'react';
|
8
|
+
import type { NavLink } from '../types';
|
9
|
+
|
10
|
+
export interface MenuItemDropProps {
|
11
|
+
link: NavLink;
|
12
|
+
}
|
13
|
+
export const MenuItemDrop: FC<MenuItemDropProps> = ({ link }) => {
|
14
|
+
const [show, changeShow] = useState(false);
|
15
|
+
const menuRef = useRef(null);
|
16
|
+
const { t } = useTranslation('nav');
|
17
|
+
|
18
|
+
useToggleClickOutSide(menuRef, () => {
|
19
|
+
changeShow(false);
|
20
|
+
});
|
21
|
+
|
22
|
+
const toggleShow = () => {
|
23
|
+
changeShow(!show);
|
24
|
+
};
|
25
|
+
|
26
|
+
if (!link || !link.show) {
|
27
|
+
return null;
|
28
|
+
}
|
29
|
+
|
30
|
+
const hasSubMenu = link.subMenus && link?.subMenus.length > 0;
|
31
|
+
|
32
|
+
return (
|
33
|
+
<div
|
34
|
+
className="relative cursor-pointer rounded-full px-3 py-2 hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10"
|
35
|
+
ref={menuRef}
|
36
|
+
>
|
37
|
+
{!hasSubMenu && (
|
38
|
+
<Link
|
39
|
+
className={`block text-black dark:text-gray-50`}
|
40
|
+
href={link?.to}
|
41
|
+
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
42
|
+
>
|
43
|
+
{link?.icon && <i className={link?.icon} />} {t(link.name)}
|
44
|
+
</Link>
|
45
|
+
)}
|
46
|
+
|
47
|
+
{hasSubMenu && (
|
48
|
+
<div
|
49
|
+
className={`block text-black dark:text-gray-50`}
|
50
|
+
onClick={toggleShow}
|
51
|
+
>
|
52
|
+
{link?.icon && <i className={link?.icon} />} {link?.name}
|
53
|
+
<i
|
54
|
+
className={`fas fa-chevron-down px-2 transition-all duration-500 ${show ? ' rotate-180' : ''}`}
|
55
|
+
></i>
|
56
|
+
</div>
|
57
|
+
)}
|
58
|
+
|
59
|
+
{/* 子菜单 */}
|
60
|
+
{hasSubMenu && (
|
61
|
+
<div
|
62
|
+
className={`${show ? 'visible top-10 opacity-100 ' : 'invisible top-8 opacity-0 '} absolute z-20 block w-40 rounded border border-gray-100 bg-white drop-shadow-lg transition-all duration-300 dark:border-gray-800 dark:bg-black`}
|
63
|
+
>
|
64
|
+
{link.subMenus?.map((sLink) => {
|
65
|
+
return (
|
66
|
+
<Link
|
67
|
+
key={sLink.id}
|
68
|
+
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"
|
69
|
+
href={sLink.to}
|
70
|
+
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
71
|
+
>
|
72
|
+
<span className="text-nowrap">
|
73
|
+
{link?.icon && <i className={sLink?.icon}> </i>}
|
74
|
+
{sLink.title}
|
75
|
+
</span>
|
76
|
+
</Link>
|
77
|
+
);
|
78
|
+
})}
|
79
|
+
</div>
|
80
|
+
)}
|
81
|
+
</div>
|
82
|
+
);
|
83
|
+
};
|
@@ -0,0 +1,50 @@
|
|
1
|
+
.stickyNav {
|
2
|
+
position: sticky;
|
3
|
+
z-index: 10;
|
4
|
+
top: 0;
|
5
|
+
backdrop-filter: blur(5px);
|
6
|
+
transition: all 0.5s cubic-bezier(0.4, 0, 0, 1);
|
7
|
+
border-bottom-color: transparent;
|
8
|
+
}
|
9
|
+
|
10
|
+
.stickyNavFull {
|
11
|
+
@apply border-b border-gray-200 border-opacity-50 dark:border-gray-600 dark:border-opacity-50;
|
12
|
+
}
|
13
|
+
|
14
|
+
.article-tags::-webkit-scrollbar {
|
15
|
+
width: 0 !important;
|
16
|
+
}
|
17
|
+
|
18
|
+
.tag-container ul::-webkit-scrollbar {
|
19
|
+
width: 0 !important;
|
20
|
+
}
|
21
|
+
|
22
|
+
.tag-container ul {
|
23
|
+
-ms-overflow-style: none;
|
24
|
+
overflow: -moz-scrollbars-none;
|
25
|
+
-moz-user-select: none;
|
26
|
+
-webkit-user-select: none;
|
27
|
+
-ms-user-select: none;
|
28
|
+
-khtml-user-select: none;
|
29
|
+
user-select: none;
|
30
|
+
}
|
31
|
+
|
32
|
+
@media (min-width: 768px) {
|
33
|
+
.stickyNavFull {
|
34
|
+
@apply max-w-full border-b border-gray-200 border-opacity-50 dark:border-gray-600 dark:border-opacity-50;
|
35
|
+
}
|
36
|
+
.headerName {
|
37
|
+
display: block;
|
38
|
+
transition: all 0.5s cubic-bezier(0.4, 0, 0, 1);
|
39
|
+
}
|
40
|
+
.stickyNavFull .headerName {
|
41
|
+
@apply text-gray-600 dark:text-gray-300;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
@supports not (backdrop-filter: none) {
|
46
|
+
.stickyNav {
|
47
|
+
backdrop-filter: none;
|
48
|
+
@apply bg-day dark:bg-gray-800;
|
49
|
+
}
|
50
|
+
}
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
2
|
+
import Link from 'next/link';
|
3
|
+
import BLOG from '@/blog.config';
|
4
|
+
import CONFIG from '../../theme.config';
|
5
|
+
import { SvgIcon } from '../SvgIcon';
|
6
|
+
import { MenuItemDrop } from '../MenuItemDrop';
|
7
|
+
import Collapse from '@/components/Collapse';
|
8
|
+
import { MenuItemCollapse } from '../MenuItemCollapse';
|
9
|
+
import LazyImage from '@/components/LazyImage';
|
10
|
+
import DarkModeButton from '../DarkModeButton';
|
11
|
+
import RandomPostButton from '../RandomPostButton';
|
12
|
+
import SearchButton from '../SearchButton';
|
13
|
+
import LanguageSwitchButton from '../LanguageSwitchButton';
|
14
|
+
import useToggleClickOutSide from '@/hooks/useToggleClickOutSide';
|
15
|
+
import { useNobeliumStore } from '../../providers';
|
16
|
+
import styles from './Nav.module.css';
|
17
|
+
|
18
|
+
import type { FC } from 'react';
|
19
|
+
import type { NavLink } from '../../types';
|
20
|
+
import type { CollapseHandle } from '@/components/Collapse';
|
21
|
+
|
22
|
+
const Nav: FC = () => {
|
23
|
+
const navRef = useRef<HTMLDivElement>(null);
|
24
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
25
|
+
const { siteInfo } = useNobeliumStore((state) => state);
|
26
|
+
|
27
|
+
const handler: IntersectionObserverCallback = ([entry]) => {
|
28
|
+
if (navRef && navRef.current) {
|
29
|
+
if (!entry.isIntersecting && entry !== undefined) {
|
30
|
+
navRef.current?.classList.add(styles.stickyNavFull);
|
31
|
+
} else {
|
32
|
+
navRef.current?.classList.remove(styles.stickyNavFull);
|
33
|
+
}
|
34
|
+
} else {
|
35
|
+
navRef.current?.classList.add('remove-sticky');
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
useEffect(() => {
|
40
|
+
const observer = new window.IntersectionObserver(handler);
|
41
|
+
const sentinelRefCurrent = sentinelRef.current;
|
42
|
+
if (sentinelRefCurrent) observer.observe(sentinelRefCurrent);
|
43
|
+
return () => {
|
44
|
+
if (sentinelRefCurrent) observer.unobserve(sentinelRefCurrent);
|
45
|
+
};
|
46
|
+
}, [sentinelRef]);
|
47
|
+
|
48
|
+
return (
|
49
|
+
<>
|
50
|
+
<div className="h-4 md:h-12" ref={sentinelRef} />
|
51
|
+
<div
|
52
|
+
className={`${styles.stickyNav} m-auto mb-2 flex h-6 w-full max-w-3xl flex-row items-center justify-between bg-opacity-60 px-4 py-8 md:mb-12`}
|
53
|
+
ref={navRef}
|
54
|
+
>
|
55
|
+
<Link
|
56
|
+
className="flex items-center rounded-full px-3 py-2 hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10"
|
57
|
+
href="/"
|
58
|
+
aria-label={BLOG.TITLE}
|
59
|
+
>
|
60
|
+
<div className="h-6 w-6">
|
61
|
+
{/* <SvgIcon/> */}
|
62
|
+
{CONFIG.NAV_NOTION_ICON ? (
|
63
|
+
<LazyImage
|
64
|
+
src={siteInfo?.icon}
|
65
|
+
width={24}
|
66
|
+
height={24}
|
67
|
+
alt={BLOG.AUTHOR}
|
68
|
+
/>
|
69
|
+
) : (
|
70
|
+
<SvgIcon />
|
71
|
+
)}
|
72
|
+
</div>
|
73
|
+
<p
|
74
|
+
className={`${styles.headerName} ml-2 overflow-hidden font-medium text-gray-800 dark:text-gray-300`}
|
75
|
+
>
|
76
|
+
{siteInfo?.title}
|
77
|
+
</p>
|
78
|
+
</Link>
|
79
|
+
<NavBar />
|
80
|
+
</div>
|
81
|
+
</>
|
82
|
+
);
|
83
|
+
};
|
84
|
+
|
85
|
+
const NavBar: FC = () => {
|
86
|
+
const [isOpen, changeOpen] = useState(false);
|
87
|
+
const collapseRef = useRef<CollapseHandle>(null);
|
88
|
+
const mobileMenuRef = useRef(null);
|
89
|
+
const mobileMenuToggleButtonRef = useRef(null);
|
90
|
+
|
91
|
+
const toggleOpen = () => {
|
92
|
+
changeOpen(!isOpen);
|
93
|
+
};
|
94
|
+
|
95
|
+
useToggleClickOutSide([mobileMenuRef, mobileMenuToggleButtonRef], () => {
|
96
|
+
changeOpen(false);
|
97
|
+
});
|
98
|
+
|
99
|
+
const links: NavLink[] = [
|
100
|
+
{
|
101
|
+
id: 'rss',
|
102
|
+
icon: 'fas fa-rss',
|
103
|
+
name: 'rss',
|
104
|
+
to: '/feed',
|
105
|
+
show: !!(BLOG.ENABLE_RSS && CONFIG.MENU_RSS),
|
106
|
+
target: '_blank',
|
107
|
+
},
|
108
|
+
{
|
109
|
+
id: 'search',
|
110
|
+
icon: 'fas fa-search',
|
111
|
+
name: 'search',
|
112
|
+
to: '/search',
|
113
|
+
show: CONFIG.MENU_SEARCH,
|
114
|
+
},
|
115
|
+
{
|
116
|
+
id: 'archive',
|
117
|
+
icon: 'fas fa-archive',
|
118
|
+
name: 'archive',
|
119
|
+
to: '/archive',
|
120
|
+
show: CONFIG.MENU_ARCHIVE,
|
121
|
+
},
|
122
|
+
{
|
123
|
+
id: 'category',
|
124
|
+
icon: 'fas fa-folder',
|
125
|
+
name: 'category',
|
126
|
+
to: '/category',
|
127
|
+
show: CONFIG.MENU_CATEGORY,
|
128
|
+
},
|
129
|
+
{
|
130
|
+
id: 'tags',
|
131
|
+
icon: 'fas fa-tag',
|
132
|
+
name: 'tags',
|
133
|
+
to: '/tag',
|
134
|
+
show: CONFIG.MENU_TAG,
|
135
|
+
},
|
136
|
+
];
|
137
|
+
|
138
|
+
if (!links || links.length === 0) {
|
139
|
+
return null;
|
140
|
+
}
|
141
|
+
|
142
|
+
return (
|
143
|
+
<div className="flex flex-shrink-0">
|
144
|
+
<ul className="hidden flex-row md:flex">
|
145
|
+
{links.map((link) => (
|
146
|
+
<MenuItemDrop key={link?.id} link={link} />
|
147
|
+
))}
|
148
|
+
</ul>
|
149
|
+
|
150
|
+
{CONFIG.MENU_RANDOM_POST && <RandomPostButton />}
|
151
|
+
{CONFIG.MENU_SEARCH_BUTTON && <SearchButton />}
|
152
|
+
{CONFIG.MENU_LANGUAGE_SWITCH && <LanguageSwitchButton />}
|
153
|
+
{CONFIG.MENU_DARK_MODE_SWITCH && <DarkModeButton />}
|
154
|
+
|
155
|
+
{/* 移动端菜单按钮 */}
|
156
|
+
<div
|
157
|
+
onClick={toggleOpen}
|
158
|
+
ref={mobileMenuToggleButtonRef}
|
159
|
+
className="flex h-10 w-10 cursor-pointer items-center justify-center md:hidden"
|
160
|
+
>
|
161
|
+
<i className="fas fa-bars"></i>
|
162
|
+
</div>
|
163
|
+
|
164
|
+
{/* 移动端使用的菜单 */}
|
165
|
+
<div className="md:hidden" ref={mobileMenuRef}>
|
166
|
+
<Collapse
|
167
|
+
collapseRef={collapseRef}
|
168
|
+
isOpen={isOpen}
|
169
|
+
type="vertical"
|
170
|
+
className="fixed right-6 top-16"
|
171
|
+
>
|
172
|
+
<div className="flex w-48 flex-col rounded border bg-white py-2 text-sm dark:border-black dark:bg-black">
|
173
|
+
{links.map((link) => (
|
174
|
+
<MenuItemCollapse
|
175
|
+
key={link?.id}
|
176
|
+
link={link}
|
177
|
+
onHeightChange={collapseRef.current?.updateCollapseHeight}
|
178
|
+
/>
|
179
|
+
))}
|
180
|
+
</div>
|
181
|
+
</Collapse>
|
182
|
+
</div>
|
183
|
+
</div>
|
184
|
+
);
|
185
|
+
};
|
186
|
+
|
187
|
+
export default Nav;
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import { useRouter } from 'next/router';
|
3
|
+
import { useTranslation } from 'next-i18next';
|
4
|
+
import { useNobeliumStore } from '../providers';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* 随机跳转到一个文章
|
8
|
+
*/
|
9
|
+
export default function RandomPostButton() {
|
10
|
+
const router = useRouter();
|
11
|
+
const { t } = useTranslation('menu');
|
12
|
+
const { latestPosts } = useNobeliumStore((state) => state);
|
13
|
+
/**
|
14
|
+
* 随机跳转文章
|
15
|
+
*/
|
16
|
+
function handleClick() {
|
17
|
+
const randomIndex = Math.floor(Math.random() * latestPosts.length);
|
18
|
+
const randomPost = latestPosts[randomIndex];
|
19
|
+
router.push(`${BLOG.SUB_PATH}/${randomPost?.slug}`);
|
20
|
+
}
|
21
|
+
|
22
|
+
return (
|
23
|
+
<div
|
24
|
+
title={t('walk-around')}
|
25
|
+
className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10"
|
26
|
+
onClick={handleClick}
|
27
|
+
>
|
28
|
+
<i className="fa-solid fa-podcast"></i>
|
29
|
+
</div>
|
30
|
+
);
|
31
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
// import BLOG from '@/blog.config';
|
2
|
+
import { useRouter } from 'next/router';
|
3
|
+
import { useTranslation } from 'next-i18next';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* 搜索按钮
|
7
|
+
* @returns
|
8
|
+
*/
|
9
|
+
export default function SearchButton() {
|
10
|
+
// const { searchModal } = useNobeliumGlobal();
|
11
|
+
const { t } = useTranslation('nav');
|
12
|
+
const router = useRouter();
|
13
|
+
|
14
|
+
function handleSearch() {
|
15
|
+
// if (BLOG.ALGOLIA_APP_ID) {
|
16
|
+
// searchModal.current.openSearch();
|
17
|
+
// } else {
|
18
|
+
router.push('/search');
|
19
|
+
// }
|
20
|
+
}
|
21
|
+
|
22
|
+
return (
|
23
|
+
<div
|
24
|
+
onClick={handleSearch}
|
25
|
+
title={t('search')}
|
26
|
+
className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-10"
|
27
|
+
>
|
28
|
+
<i title={t('search')} className="fa-solid fa-magnifying-glass" />
|
29
|
+
</div>
|
30
|
+
);
|
31
|
+
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import { useRouter } from 'next/router';
|
2
|
+
import { type FC, useRef, useState } from 'react';
|
3
|
+
import { useTranslation } from 'next-i18next';
|
4
|
+
|
5
|
+
export interface SearchInputProps {
|
6
|
+
tag: string;
|
7
|
+
keyword: string;
|
8
|
+
}
|
9
|
+
|
10
|
+
const SearchInput: FC<SearchInputProps> = ({ tag, keyword }) => {
|
11
|
+
const router = useRouter();
|
12
|
+
const { t } = useTranslation('common');
|
13
|
+
const [showClean, setShowClean] = useState(false);
|
14
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
15
|
+
const lock = useRef(false);
|
16
|
+
|
17
|
+
const handleSearch = () => {
|
18
|
+
const key = searchInputRef.current?.value.trim();
|
19
|
+
if (key) {
|
20
|
+
router.push(`/search/${key}`);
|
21
|
+
} else {
|
22
|
+
router.push('/');
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
27
|
+
if (e.key === 'Enter') handleSearch();
|
28
|
+
else if (e.key === 'Escape') cleanSearch();
|
29
|
+
};
|
30
|
+
|
31
|
+
const cleanSearch = () => {
|
32
|
+
(searchInputRef.current as HTMLInputElement).value = '';
|
33
|
+
setShowClean(false);
|
34
|
+
};
|
35
|
+
|
36
|
+
function lockSearchInput() {
|
37
|
+
lock.current = true;
|
38
|
+
}
|
39
|
+
|
40
|
+
function unLockSearchInput() {
|
41
|
+
lock.current = false;
|
42
|
+
}
|
43
|
+
const updateSearchKey = () => {
|
44
|
+
if (lock.current) {
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
if (searchInputRef.current?.value) {
|
48
|
+
setShowClean(true);
|
49
|
+
} else {
|
50
|
+
setShowClean(false);
|
51
|
+
}
|
52
|
+
};
|
53
|
+
|
54
|
+
return (
|
55
|
+
<section className="flex w-full bg-gray-100">
|
56
|
+
<input
|
57
|
+
ref={searchInputRef}
|
58
|
+
type="text"
|
59
|
+
placeholder={tag ? `${t('tags')} #${tag}` : `${t('search-articles')}`}
|
60
|
+
className={
|
61
|
+
'w-full bg-gray-100 pl-4 text-sm font-light leading-10 text-black outline-none transition focus:shadow-lg dark:bg-gray-900 dark:text-white'
|
62
|
+
}
|
63
|
+
onKeyUp={handleKeyUp}
|
64
|
+
onCompositionStart={lockSearchInput}
|
65
|
+
onCompositionUpdate={lockSearchInput}
|
66
|
+
onCompositionEnd={unLockSearchInput}
|
67
|
+
onChange={updateSearchKey}
|
68
|
+
defaultValue={keyword || ''}
|
69
|
+
/>
|
70
|
+
|
71
|
+
<div
|
72
|
+
className="float-right -ml-8 cursor-pointer items-center justify-center py-2"
|
73
|
+
onClick={handleSearch}
|
74
|
+
>
|
75
|
+
<i
|
76
|
+
className={
|
77
|
+
'fas fa-search transform cursor-pointer text-gray-500 duration-200 hover:text-black'
|
78
|
+
}
|
79
|
+
/>
|
80
|
+
</div>
|
81
|
+
|
82
|
+
{showClean && (
|
83
|
+
<div className="float-right -ml-12 cursor-pointer items-center justify-center py-2 dark:bg-gray-600 dark:hover:bg-gray-800">
|
84
|
+
<i
|
85
|
+
className="fas fa-times transform cursor-pointer text-gray-400 duration-200 hover:text-black"
|
86
|
+
onClick={cleanSearch}
|
87
|
+
/>
|
88
|
+
</div>
|
89
|
+
)}
|
90
|
+
</section>
|
91
|
+
);
|
92
|
+
};
|
93
|
+
|
94
|
+
export default SearchInput;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import SearchInput from './SearchInput';
|
2
|
+
import Tags from './Tags';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* 搜索页面上方嵌入内容
|
6
|
+
* @param {*} props
|
7
|
+
* @returns
|
8
|
+
*/
|
9
|
+
export default function SearchNavBar(props) {
|
10
|
+
return (
|
11
|
+
<>
|
12
|
+
<div className="pb-12">
|
13
|
+
<SearchInput {...props} />
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<Tags tagOptions={props.tagOptions} tag={props.keyword} />
|
17
|
+
</>
|
18
|
+
);
|
19
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import Live2D from '@/components/Live2D';
|
3
|
+
import Link from 'next/link';
|
4
|
+
import dynamic from 'next/dynamic';
|
5
|
+
import { useTranslation } from 'next-i18next';
|
6
|
+
|
7
|
+
const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'));
|
8
|
+
|
9
|
+
export const SideBar = (props) => {
|
10
|
+
const { latestPosts, categories } = props;
|
11
|
+
const { t } = useTranslation('common');
|
12
|
+
|
13
|
+
return (
|
14
|
+
<div className="sticky top-8 w-full md:w-64">
|
15
|
+
<aside className="mb-6 overflow-hidden rounded shadow">
|
16
|
+
<h3 className="border-b bg-gray-100 px-4 py-3 text-sm text-gray-700 dark:border-hexo-black-gray dark:bg-hexo-black-gray dark:text-gray-200">
|
17
|
+
{t('category')}
|
18
|
+
</h3>
|
19
|
+
|
20
|
+
<div className="p-4">
|
21
|
+
<ul className="list-reset leading-normal">
|
22
|
+
{categories?.map((category) => {
|
23
|
+
return (
|
24
|
+
<Link
|
25
|
+
key={category.name}
|
26
|
+
href={`/category/${category.name}`}
|
27
|
+
passHref
|
28
|
+
legacyBehavior
|
29
|
+
>
|
30
|
+
<li>
|
31
|
+
{' '}
|
32
|
+
<a href="#" className="text-gray-darkest text-sm">
|
33
|
+
{category.name}({category.count})
|
34
|
+
</a>
|
35
|
+
</li>
|
36
|
+
</Link>
|
37
|
+
);
|
38
|
+
})}
|
39
|
+
</ul>
|
40
|
+
</div>
|
41
|
+
</aside>
|
42
|
+
|
43
|
+
<aside className="mb-6 overflow-hidden rounded shadow">
|
44
|
+
<h3 className="border-b bg-gray-100 px-4 py-3 text-sm text-gray-700 dark:border-hexo-black-gray dark:bg-hexo-black-gray dark:text-gray-200">
|
45
|
+
{t('latest-posts')}
|
46
|
+
</h3>
|
47
|
+
|
48
|
+
<div className="p-4">
|
49
|
+
<ul className="list-reset leading-normal">
|
50
|
+
{latestPosts?.map((p) => {
|
51
|
+
return (
|
52
|
+
<Link key={p.id} href={`/${p.slug}`} passHref legacyBehavior>
|
53
|
+
<li>
|
54
|
+
{' '}
|
55
|
+
<a href="#" className="text-gray-darkest text-sm">
|
56
|
+
{p.title}
|
57
|
+
</a>
|
58
|
+
</li>
|
59
|
+
</Link>
|
60
|
+
);
|
61
|
+
})}
|
62
|
+
</ul>
|
63
|
+
</div>
|
64
|
+
</aside>
|
65
|
+
|
66
|
+
{BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT && (
|
67
|
+
<aside className="mb-6 overflow-hidden rounded shadow">
|
68
|
+
<h3 className="border-b bg-gray-100 px-4 py-3 text-sm text-gray-700 dark:border-hexo-black-gray dark:bg-hexo-black-gray dark:text-gray-200">
|
69
|
+
{t('recent-comments')}
|
70
|
+
</h3>
|
71
|
+
|
72
|
+
<div className="p-4">
|
73
|
+
<ExampleRecentComments />
|
74
|
+
</div>
|
75
|
+
</aside>
|
76
|
+
)}
|
77
|
+
|
78
|
+
<aside className="mb-6 overflow-hidden rounded">
|
79
|
+
<Live2D />
|
80
|
+
</aside>
|
81
|
+
</div>
|
82
|
+
);
|
83
|
+
};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
export const SvgIcon = () => {
|
2
|
+
return <svg
|
3
|
+
width="24"
|
4
|
+
height="24"
|
5
|
+
viewBox="0 0 24 24"
|
6
|
+
fill="none"
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
8
|
+
>
|
9
|
+
<rect
|
10
|
+
width="24"
|
11
|
+
height="24"
|
12
|
+
className="fill-current text-black dark:text-white"
|
13
|
+
/>
|
14
|
+
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
15
|
+
<defs>
|
16
|
+
<radialGradient
|
17
|
+
id="paint0_radial"
|
18
|
+
cx="0"
|
19
|
+
cy="0"
|
20
|
+
r="1"
|
21
|
+
gradientUnits="userSpaceOnUse"
|
22
|
+
gradientTransform="rotate(45) scale(39.598)"
|
23
|
+
>
|
24
|
+
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
25
|
+
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
26
|
+
</radialGradient>
|
27
|
+
</defs>
|
28
|
+
</svg>
|
29
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import Link from 'next/link'
|
2
|
+
|
3
|
+
const TagItem = ({ tag }) => (
|
4
|
+
(<Link href={`/tag/${encodeURIComponent(tag)}`}>
|
5
|
+
|
6
|
+
<p className="mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600">
|
7
|
+
{tag}
|
8
|
+
</p>
|
9
|
+
|
10
|
+
</Link>)
|
11
|
+
)
|
12
|
+
|
13
|
+
export default TagItem
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import Link from 'next/link';
|
2
|
+
import type { TagInfo } from '@/lib/notion/types';
|
3
|
+
import type { FC } from 'react';
|
4
|
+
|
5
|
+
export interface TagsProps {
|
6
|
+
tagOptions: TagInfo[];
|
7
|
+
tag: string;
|
8
|
+
}
|
9
|
+
|
10
|
+
const Tags: FC<TagsProps> = ({ tagOptions, tag: currentTag }) => {
|
11
|
+
if (!tagOptions) return null;
|
12
|
+
return (
|
13
|
+
<div className="tag-container">
|
14
|
+
<ul className="mt-4 flex max-w-full overflow-x-auto">
|
15
|
+
{tagOptions.map((tag) => {
|
16
|
+
return (
|
17
|
+
<li
|
18
|
+
key={tag.id}
|
19
|
+
className={`mr-3 whitespace-nowrap border font-medium dark:text-gray-300 ${
|
20
|
+
tag.name === currentTag
|
21
|
+
? 'border-black bg-black text-white dark:border-gray-600 dark:bg-gray-600'
|
22
|
+
: 'border-gray-100 bg-gray-100 text-gray-400 dark:border-gray-800 dark:bg-night'
|
23
|
+
}`}
|
24
|
+
>
|
25
|
+
<Link
|
26
|
+
key={tag.id}
|
27
|
+
href={
|
28
|
+
tag.name === currentTag
|
29
|
+
? '/search'
|
30
|
+
: `/tag/${encodeURIComponent(tag.name)}`
|
31
|
+
}
|
32
|
+
className="block px-4 py-2"
|
33
|
+
>
|
34
|
+
{`${tag.name} (${tag.count})`}
|
35
|
+
</Link>
|
36
|
+
</li>
|
37
|
+
);
|
38
|
+
})}
|
39
|
+
</ul>
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
export default Tags;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import BLOG from '@/blog.config'
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 标题栏
|
5
|
+
* @param {*} props
|
6
|
+
* @returns
|
7
|
+
*/
|
8
|
+
export const Title = (props) => {
|
9
|
+
const { siteInfo, post } = props
|
10
|
+
const title = post?.title || siteInfo?.description
|
11
|
+
const description = post?.description || BLOG.AUTHOR
|
12
|
+
|
13
|
+
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
|
14
|
+
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
|
15
|
+
<p className="leading-loose text-gray-dark">
|
16
|
+
{description}
|
17
|
+
</p>
|
18
|
+
</div>
|
19
|
+
}
|