From 99b5383f84478f6d6aaaff1ca1a3dc39bc876b73 Mon Sep 17 00:00:00 2001 From: MH Hung Date: Wed, 17 Sep 2025 09:37:16 +0800 Subject: [PATCH] feat(theme): init project --- mhhung/components/ArticleAround.js | 32 ++ mhhung/components/ArticleInfo.js | 65 ++++ mhhung/components/ArticleLock.js | 52 +++ mhhung/components/BlogArchiveItem.js | 36 ++ mhhung/components/BlogItem.js | 101 ++++++ mhhung/components/BlogListPage.js | 74 ++++ mhhung/components/BlogListScroll.js | 70 ++++ mhhung/components/BlogPostBar.js | 29 ++ mhhung/components/Catalog.js | 136 ++++++++ mhhung/components/ExampleRecentComments.js | 35 ++ mhhung/components/Footer.js | 29 ++ mhhung/components/JumpToTopButton.js | 35 ++ mhhung/components/MenuItemCollapse.js | 92 +++++ mhhung/components/MenuItemDrop.js | 78 +++++ mhhung/components/MenuList.js | 83 +++++ mhhung/components/NavBar.js | 37 ++ mhhung/components/RecommendPosts.js | 32 ++ mhhung/components/SocialButton.js | 115 +++++++ mhhung/components/Title.js | 19 ++ mhhung/components/TopBar.js | 19 ++ mhhung/config.js | 17 + mhhung/index.js | 372 +++++++++++++++++++++ mhhung/style.js | 93 ++++++ 23 files changed, 1651 insertions(+) create mode 100644 mhhung/components/ArticleAround.js create mode 100644 mhhung/components/ArticleInfo.js create mode 100644 mhhung/components/ArticleLock.js create mode 100644 mhhung/components/BlogArchiveItem.js create mode 100644 mhhung/components/BlogItem.js create mode 100644 mhhung/components/BlogListPage.js create mode 100644 mhhung/components/BlogListScroll.js create mode 100644 mhhung/components/BlogPostBar.js create mode 100644 mhhung/components/Catalog.js create mode 100644 mhhung/components/ExampleRecentComments.js create mode 100644 mhhung/components/Footer.js create mode 100644 mhhung/components/JumpToTopButton.js create mode 100644 mhhung/components/MenuItemCollapse.js create mode 100644 mhhung/components/MenuItemDrop.js create mode 100644 mhhung/components/MenuList.js create mode 100644 mhhung/components/NavBar.js create mode 100644 mhhung/components/RecommendPosts.js create mode 100644 mhhung/components/SocialButton.js create mode 100644 mhhung/components/Title.js create mode 100644 mhhung/components/TopBar.js create mode 100644 mhhung/config.js create mode 100644 mhhung/index.js create mode 100644 mhhung/style.js diff --git a/mhhung/components/ArticleAround.js b/mhhung/components/ArticleAround.js new file mode 100644 index 0000000..a2f9c7d --- /dev/null +++ b/mhhung/components/ArticleAround.js @@ -0,0 +1,32 @@ +import SmartLink from '@/components/SmartLink' + +/** + * 上一篇,下一篇文章 + * @param {prev,next} param0 + * @returns + */ +export default function ArticleAround({ prev, next }) { + if (!prev || !next) { + return <> + } + return ( +
+ {prev && + + {prev.title} + + } + {next && + {next.title} + + + } +
+ ) +} diff --git a/mhhung/components/ArticleInfo.js b/mhhung/components/ArticleInfo.js new file mode 100644 index 0000000..0065500 --- /dev/null +++ b/mhhung/components/ArticleInfo.js @@ -0,0 +1,65 @@ +import SmartLink from '@/components/SmartLink' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { siteConfig } from '@/lib/config' +import { formatDateFmt } from '@/lib/utils/formatDate' +import NotionIcon from '@/components/NotionIcon' + +/** + * 文章描述 + * @param {*} props + * @returns + */ +export default function ArticleInfo(props) { + const { post } = props + + const { locale } = useGlobal() + + return ( +
+

+ {siteConfig('POST_TITLE_ICON') && } + {post?.title} +

+ +
+ {post?.type !== 'Page' && ( +
+
+ + 发布于 + + {post.date?.start_date || post.createdTime} + + +
+ +
+ {/* {post.category && ( + + {' '} + + + {post.category} + + + )} */} + {post?.tags && + post?.tags?.length > 0 && + post?.tags.map(t => ( + + #{t} + + ))} +
+
+ )} +
+
+ ) +} diff --git a/mhhung/components/ArticleLock.js b/mhhung/components/ArticleLock.js new file mode 100644 index 0000000..d9f6e97 --- /dev/null +++ b/mhhung/components/ArticleLock.js @@ -0,0 +1,52 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export default function ArticleLock (props) { + const { validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
${locale.COMMON.PASSWORD_ERROR}
` + } + } + } + const passwordInputRef = useRef(null) + useEffect(() => { + // 选中密码输入框并将其聚焦 + passwordInputRef.current.focus() + }, []) + + return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+ { + if (e.key === 'Enter') { + submitPassword() + } + }} + ref={passwordInputRef} // 绑定ref到passwordInputRef变量 + className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50' + > +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+
+} diff --git a/mhhung/components/BlogArchiveItem.js b/mhhung/components/BlogArchiveItem.js new file mode 100644 index 0000000..28bab81 --- /dev/null +++ b/mhhung/components/BlogArchiveItem.js @@ -0,0 +1,36 @@ +import SmartLink from '@/components/SmartLink' + +/** + * 归档分组文章 + * @param {*} param0 + * @returns + */ +export default function BlogArchiveItem({ archiveTitle, archivePosts }) { + return ( +
+
+ {archiveTitle} +
+ +
    + {archivePosts.map(post => { + return ( +
  • +
    + + {post.title} + + {post.date?.start_date} +
    +
  • + ) + })} +
+
+ ) +} diff --git a/mhhung/components/BlogItem.js b/mhhung/components/BlogItem.js new file mode 100644 index 0000000..be2ac0e --- /dev/null +++ b/mhhung/components/BlogItem.js @@ -0,0 +1,101 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import NotionPage from '@/components/NotionPage' +import TwikooCommentCount from '@/components/TwikooCommentCount' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import SmartLink from '@/components/SmartLink' +import CONFIG from '../config' + +export const BlogItem = props => { + const { post } = props + const { NOTION_CONFIG } = useGlobal() + const showPageCover = siteConfig('TYPOGRAPHY_POST_COVER_ENABLE', false, CONFIG) + const showPreview = + siteConfig('POST_LIST_PREVIEW', false, NOTION_CONFIG) && post.blockMap + return ( +
+ {/* 文章标题 */} + +
+
+ {/* 图片封面 */} + {showPageCover && ( +
+ + + +
+ )} +
+ +
+

+ + {siteConfig('POST_TITLE_ICON') && ( + + )} + {post.title} + +

+ + {/* 文章信息 */} +
+
+ + 发布于 + + {post.date?.start_date || post.createdTime} + + +
+ +
+ {/* {post.category && ( + + {' '} + + + {post.category} + + + )} */} + {post?.tags && + post?.tags?.length > 0 && + post?.tags.map(t => ( + + #{t} + + ))} +
+
+ +
+ {!showPreview && ( + <> + {post.summary} + + )} + {showPreview && post?.blockMap && ( +
+ +
+
+ )} +
+
+
+
+ ) +} diff --git a/mhhung/components/BlogListPage.js b/mhhung/components/BlogListPage.js new file mode 100644 index 0000000..333bbb8 --- /dev/null +++ b/mhhung/components/BlogListPage.js @@ -0,0 +1,74 @@ +import { AdSlot } from '@/components/GoogleAdsense' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import SmartLink from '@/components/SmartLink' +import { useRouter } from 'next/router' +import CONFIG from '../config' +import { BlogItem } from './BlogItem' + +/** + * 博客列表 + * @param {*} props + * @returns + */ +export default function BlogListPage(props) { + const { page = 1, posts, postCount } = props + const router = useRouter() + const { NOTION_CONFIG } = useGlobal() + const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) + const totalPage = Math.ceil(postCount / POSTS_PER_PAGE) + const currentPage = +page + + // 博客列表嵌入广告 + const TYPOGRAPHY_POST_AD_ENABLE = siteConfig( + 'TYPOGRAPHY_POST_AD_ENABLE', + false, + CONFIG + ) + + const showPrev = currentPage > 1 + const showNext = page < totalPage + const pagePrefix = router.asPath + .split('?')[0] + .replace(/\/page\/[1-9]\d*/, '') + .replace(/\/$/, '') + .replace('.html', '') + + return ( +
+
+ {posts?.map((p, index) => ( +
+ {TYPOGRAPHY_POST_AD_ENABLE && (index + 1) % 3 === 0 && ( + + )} + {TYPOGRAPHY_POST_AD_ENABLE && index + 1 === 4 && } + +
+ ))} +
+ +
+ + NEWER POSTS + + + OLDER POSTS + +
+
+ ) +} diff --git a/mhhung/components/BlogListScroll.js b/mhhung/components/BlogListScroll.js new file mode 100644 index 0000000..2252a99 --- /dev/null +++ b/mhhung/components/BlogListScroll.js @@ -0,0 +1,70 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useRef, useState } from 'react' +import { BlogItem } from './BlogItem' + +/** + * 滚动博客列表 + * @param {*} props + * @returns + */ +export default function BlogListScroll(props) { + const { posts } = props + const { locale, NOTION_CONFIG } = useGlobal() + const [page, updatePage] = useState(1) + const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) + let hasMore = false + const postsToShow = posts + ? Object.assign(posts).slice(0, POSTS_PER_PAGE * page) + : [] + + if (posts) { + const totalCount = posts.length + hasMore = page * POSTS_PER_PAGE < totalCount + } + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + const targetRef = useRef(null) + + // 监听滚动自动分页加载 + const scrollTrigger = useCallback( + throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef + ? targetRef.current + ? targetRef.current.clientHeight + : 0 + : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500) + ) + + useEffect(() => { + window.addEventListener('scroll', scrollTrigger) + + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }) + + return ( +
+ {postsToShow.map(p => ( + + ))} + +
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+ ) +} diff --git a/mhhung/components/BlogPostBar.js b/mhhung/components/BlogPostBar.js new file mode 100644 index 0000000..9efac11 --- /dev/null +++ b/mhhung/components/BlogPostBar.js @@ -0,0 +1,29 @@ +import { useGlobal } from '@/lib/global' + +/** + * 文章列表上方嵌入 + * @param {*} props + * @returns + */ +export default function BlogPostBar(props) { + const { tag, category } = props + const { locale } = useGlobal() + + if (tag) { + return ( +
+ + {locale.COMMON.TAGS}: {tag} +
+ ) + } else if (category) { + return ( +
+ + {locale.COMMON.CATEGORY}: {category} +
+ ) + } else { + return <> + } +} diff --git a/mhhung/components/Catalog.js b/mhhung/components/Catalog.js new file mode 100644 index 0000000..e92e586 --- /dev/null +++ b/mhhung/components/Catalog.js @@ -0,0 +1,136 @@ +import { useGlobal } from '@/lib/global' +import throttle from 'lodash.throttle' +import { uuidToId } from 'notion-utils' +import { useEffect, useRef, useState } from 'react' + +/** + * 目录导航组件 + * @param toc + * @returns {JSX.Element} + * @constructor + */ +const Catalog = ({ post }) => { + const { locale } = useGlobal() + // 目录自动滚动 + const tRef = useRef(null) + // 同步选中目录事件 + const [activeSection, setActiveSection] = useState(null) + + // 监听滚动事件 + useEffect(() => { + // 如果没有文章或目录,不执行任何操作 + if (!post || !post?.toc || post?.toc?.length < 1) { + return + } + + const throttleMs = 100 // 降低节流时间提高响应速度 + + const actionSectionScrollSpy = throttle(() => { + const sections = document.getElementsByClassName('notion-h') + if (!sections || sections.length === 0) return + + let prevBBox = null + let currentSectionId = null + + // 先检查当前视口中的所有标题 + for (let i = 0; i < sections.length; ++i) { + const section = sections[i] + if (!section || !(section instanceof Element)) continue + + const bbox = section.getBoundingClientRect() + const offset = 100 // 固定偏移量,避免计算不稳定 + + // 如果标题在视口上方或接近顶部,认为是当前标题 + if (bbox.top - offset < 0) { + currentSectionId = section.getAttribute('data-id') + prevBBox = bbox + } else { + // 找到第一个在视口下方的标题就停止 + break + } + } + + // 如果没找到任何标题在视口上方,使用第一个标题 + if (!currentSectionId && sections.length > 0) { + currentSectionId = sections[0].getAttribute('data-id') + } + + // 只有当 ID 变化时才更新状态,减少不必要的渲染 + if (currentSectionId !== activeSection) { + setActiveSection(currentSectionId) + + // 查找目录中对应的索引并滚动 + const index = post?.toc?.findIndex( + obj => uuidToId(obj.id) === currentSectionId + ) + + if (index !== -1 && tRef?.current) { + tRef.current.scrollTo({ top: 28 * index, behavior: 'smooth' }) + } + } + }, throttleMs) + + const content = document.querySelector('#container-inner') + if (!content) return // 防止 content 不存在 + + // 添加滚动和内容变化的监听 + content.addEventListener('scroll', actionSectionScrollSpy) + + // 初始执行一次 + setTimeout(() => { + actionSectionScrollSpy() + }, 300) // 延迟执行确保 DOM 已完全加载 + + return () => { + content?.removeEventListener('scroll', actionSectionScrollSpy) + } + }, [post]) + + // 无目录就直接返回空 + if (!post || !post?.toc || post?.toc?.length < 1) { + return <> + } + + return ( +
+
+ + {locale.COMMON.TABLE_OF_CONTENTS} +
+ +
+ +
+
+ ) +} + +export default Catalog diff --git a/mhhung/components/ExampleRecentComments.js b/mhhung/components/ExampleRecentComments.js new file mode 100644 index 0000000..663151a --- /dev/null +++ b/mhhung/components/ExampleRecentComments.js @@ -0,0 +1,35 @@ +import SmartLink from '@/components/SmartLink' +import { RecentComments } from '@waline/client' +import { useEffect, useState } from 'react' +import { siteConfig } from '@/lib/config' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const ExampleRecentComments = (props) => { + const [comments, updateComments] = useState([]) + const [onLoading, changeLoading] = useState(true) + useEffect(() => { + RecentComments({ + serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'), + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return <> + {onLoading &&
Loading...
} + {!onLoading && comments && comments.length === 0 &&
No Comments
} + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
--{comment.nick}
+
)} + + +} + +export default ExampleRecentComments diff --git a/mhhung/components/Footer.js b/mhhung/components/Footer.js new file mode 100644 index 0000000..8d03d7f --- /dev/null +++ b/mhhung/components/Footer.js @@ -0,0 +1,29 @@ +import { BeiAnGongAn } from '@/components/BeiAnGongAn' +import DarkModeButton from '@/components/DarkModeButton' +import { siteConfig } from '@/lib/config' + +/** + * 页脚 + * @param {*} props + * @returns + */ +export default function Footer(props) { + const d = new Date() + const currentYear = d.getFullYear() + const since = siteConfig('SINCE') + const copyrightDate = + parseInt(since) < currentYear ? since + '-' + currentYear : currentYear + + return ( +
+ + +
+
+ ©{`${copyrightDate}`} {siteConfig('AUTHOR')}. +
+
All rights reserved.
+
+
+ ) +} diff --git a/mhhung/components/JumpToTopButton.js b/mhhung/components/JumpToTopButton.js new file mode 100644 index 0000000..358c937 --- /dev/null +++ b/mhhung/components/JumpToTopButton.js @@ -0,0 +1,35 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useState } from 'react' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = () => { + const { locale } = useGlobal() + const [show, switchShow] = useState(false) + const scrollListener = () => { + const scrollY = window.pageYOffset + const shouldShow = scrollY > 200 + if (shouldShow !== show) { + switchShow(shouldShow) + } + } + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, [show]) + + return
window.scrollTo({ top: 0, behavior: 'smooth' })} + > +
+} + +export default JumpToTopButton diff --git a/mhhung/components/MenuItemCollapse.js b/mhhung/components/MenuItemCollapse.js new file mode 100644 index 0000000..9de40a7 --- /dev/null +++ b/mhhung/components/MenuItemCollapse.js @@ -0,0 +1,92 @@ +import Collapse from '@/components/Collapse' +import SmartLink from '@/components/SmartLink' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = props => { + const { link } = props + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + const [isOpen, changeIsOpen] = useState(false) + + const toggleShow = () => { + changeShow(!show) + } + + const toggleOpenSubMenu = () => { + changeIsOpen(!isOpen) + } + + if (!link || !link.show) { + return null + } + + return ( + <> +
+ {!hasSubMenu && ( + + + {link?.icon && ( + + + + )} + {link?.name} + + + )} + {hasSubMenu && ( +
+ + {link?.icon && ( + + + + )} + {link?.name} + + +
+ )} +
+ + {/* 折叠子菜单 */} + {hasSubMenu && ( + + {link.subMenus.map((sLink, index) => { + return ( +
+ + + {sLink?.icon && ( + + + + )} + {sLink.title} + + +
+ ) + })} +
+ )} + + ) +} diff --git a/mhhung/components/MenuItemDrop.js b/mhhung/components/MenuItemDrop.js new file mode 100644 index 0000000..fa718f1 --- /dev/null +++ b/mhhung/components/MenuItemDrop.js @@ -0,0 +1,78 @@ +import SmartLink from '@/components/SmartLink' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const hasSubMenu = link?.subMenus?.length > 0 + const [show, changeShow] = useState(false) + const router = useRouter() + + + + if (!link || !link.show) { + return null + } + const selected = router.pathname === link.href || router.asPath === link.href + + + return ( +
+ {!hasSubMenu && ( + + {link?.name} + + ) + } + + + {hasSubMenu && ( + <> +
changeShow(true)} + onMouseOut={() => changeShow(false)} + className={ + 'relative ' + + (selected + ? 'bg-green-600 text-white hover:text-white' + : 'hover:text-green-600') + }> +
+ + {link?.icon && } {link?.name} + + {hasSubMenu && ( + + )} +
+ + {/* 子菜單 */} +
    + {link?.subMenus?.map((sLink, index) => { + return ( +
  • + + + {link?.icon &&   } + {sLink.title} + + +
  • + ) + })} +
+
+ + + )} + +
+ + ) +} diff --git a/mhhung/components/MenuList.js b/mhhung/components/MenuList.js new file mode 100644 index 0000000..8eb458d --- /dev/null +++ b/mhhung/components/MenuList.js @@ -0,0 +1,83 @@ +import Collapse from '@/components/Collapse' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { useRouter } from 'next/router' +import { useEffect, useRef, useState } from 'react' +import CONFIG from '../config' +import { MenuItemCollapse } from './MenuItemCollapse' +import { MenuItemDrop } from './MenuItemDrop' + +/** + * 菜单导航 + * @param {*} props + * @returns + */ +export const MenuList = ({ customNav, customMenu }) => { + const { locale } = useGlobal() + const [isOpen, changeIsOpen] = useState(false) + const toggleIsOpen = () => { + changeIsOpen(!isOpen) + } + const closeMenu = e => { + changeIsOpen(false) + } + const router = useRouter() + const collapseRef = useRef(null) + + useEffect(() => { + router.events.on('routeChangeStart', closeMenu) + }) + + let links = [ + { + icon: 'fas fa-archive', + name: locale.NAV.ARCHIVE, + href: '/archive', + show: siteConfig('TYPOGRAPHY_MENU_ARCHIVE', null, CONFIG) + }, + { + icon: 'fas fa-folder', + name: locale.COMMON.CATEGORY, + href: '/category', + show: siteConfig('TYPOGRAPHY_MENU_CATEGORY', null, CONFIG) + }, + { + icon: 'fas fa-tag', + name: locale.COMMON.TAGS, + href: '/tag', + show: siteConfig('TYPOGRAPHY_MENU_TAG', null, CONFIG) + } + ] + + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则覆盖 Page 生成的菜单 + if (siteConfig('CUSTOM_MENU')) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( + <> + {/* 大屏模式菜单 - 垂直排列 */} + + {/* 移动端小屏菜单 - 水平排列 */} + + + ) +} diff --git a/mhhung/components/NavBar.js b/mhhung/components/NavBar.js new file mode 100644 index 0000000..e13a720 --- /dev/null +++ b/mhhung/components/NavBar.js @@ -0,0 +1,37 @@ +import { siteConfig } from '@/lib/config' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { useSimpleGlobal } from '..' +import { MenuList } from './MenuList' +import SocialButton from './SocialButton' +import SmartLink from '@/components/SmartLink' + +/** + * 菜单导航 + * @param {*} props + * @returns + */ +export default function NavBar(props) { + return ( +
+
+ +
+
+ {siteConfig('TYPOGRAPHY_BLOG_NAME')} +
+
+ {siteConfig('TYPOGRAPHY_BLOG_NAME_EN')} +
+
+
+
+ +
+ ) +} diff --git a/mhhung/components/RecommendPosts.js b/mhhung/components/RecommendPosts.js new file mode 100644 index 0000000..43f2d28 --- /dev/null +++ b/mhhung/components/RecommendPosts.js @@ -0,0 +1,32 @@ +import SmartLink from '@/components/SmartLink' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { siteConfig } from '@/lib/config' + +/** + * 展示文章推荐 + */ +const RecommendPosts = ({ recommendPosts }) => { + const { locale } = useGlobal() + if (!siteConfig('TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS', null, CONFIG) || !recommendPosts || recommendPosts.length < 1) { + return <> + } + + return ( +
+
{locale.COMMON.RELATE_POSTS} :
+
    + {recommendPosts.map(post => ( +
  • + + + {post.title} + + +
  • + ))} +
+
+ ) +} +export default RecommendPosts diff --git a/mhhung/components/SocialButton.js b/mhhung/components/SocialButton.js new file mode 100644 index 0000000..ced5c85 --- /dev/null +++ b/mhhung/components/SocialButton.js @@ -0,0 +1,115 @@ +import { siteConfig } from '@/lib/config' + +/** + * 社交联系方式按钮组 + * @returns {JSX.Element} + * @constructor + */ +const SocialButton = () => { + return ( +
+
+ {siteConfig('CONTACT_GITHUB') && ( + + + + )} + {siteConfig('CONTACT_TWITTER') && ( + + + + )} + {siteConfig('CONTACT_TELEGRAM') && ( + + + + )} + {siteConfig('CONTACT_LINKEDIN') && ( + + + + )} + {siteConfig('CONTACT_WEIBO') && ( + + + + )} + {siteConfig('CONTACT_INSTAGRAM') && ( + + + + )} + {siteConfig('CONTACT_EMAIL') && ( + + + + )} + {JSON.parse(siteConfig('ENABLE_RSS')) && ( + + + + )} + {siteConfig('CONTACT_BILIBILI') && ( + + + + )} + {siteConfig('CONTACT_YOUTUBE') && ( + + + + )} + {siteConfig('CONTACT_THREADS') && ( + + + + )} +
+
+ ) +} +export default SocialButton diff --git a/mhhung/components/Title.js b/mhhung/components/Title.js new file mode 100644 index 0000000..151d736 --- /dev/null +++ b/mhhung/components/Title.js @@ -0,0 +1,19 @@ +import { siteConfig } from '@/lib/config' + +/** + * 标题栏 + * @param {*} props + * @returns + */ +export const Title = (props) => { + const { post } = props + const title = post?.title || siteConfig('DESCRIPTION') + const description = post?.description || siteConfig('AUTHOR') + + return
+

{title}

+

+ {description} +

+
+} diff --git a/mhhung/components/TopBar.js b/mhhung/components/TopBar.js new file mode 100644 index 0000000..e9224de --- /dev/null +++ b/mhhung/components/TopBar.js @@ -0,0 +1,19 @@ +import CONFIG from '../config' +import { siteConfig } from '@/lib/config' + +/** + * 网站顶部 提示栏 + * @returns + */ +export default function TopBar (props) { + const content = siteConfig('SIMPLE_TOP_BAR_CONTENT', null, CONFIG) + + if (content) { + return
+
+
+
+
+ } + return <> +} diff --git a/mhhung/config.js b/mhhung/config.js new file mode 100644 index 0000000..1f7d095 --- /dev/null +++ b/mhhung/config.js @@ -0,0 +1,17 @@ +const CONFIG = { + // 博客標題 雙語言 + TYPOGRAPHY_BLOG_NAME: process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME || '活字印刷', + TYPOGRAPHY_BLOG_NAME_EN: process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME_EN || process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME || 'Typography', + + TYPOGRAPHY_POST_AD_ENABLE: process.env.NEXT_PUBLIC_TYPOGRAPHY_POST_AD_ENABLE || false, // 文章列表是否插入广告 + + TYPOGRAPHY_POST_COVER_ENABLE: process.env.NEXT_PUBLIC_TYPOGRAPHY_POST_COVER_ENABLE || false, // 是否展示博客封面 + + TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS: process.env.NEXT_PUBLIC_TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS || true, // 文章详情底部显示推荐 + + // 菜单配置 + TYPOGRAPHY_MENU_CATEGORY: true, // 显示分类 + TYPOGRAPHY_MENU_TAG: true, // 显示标签 + TYPOGRAPHY_MENU_ARCHIVE: true, // 显示归档 +} +export default CONFIG diff --git a/mhhung/index.js b/mhhung/index.js new file mode 100644 index 0000000..b5fa092 --- /dev/null +++ b/mhhung/index.js @@ -0,0 +1,372 @@ +import { AdSlot } from '@/components/GoogleAdsense' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { isBrowser } from '@/lib/utils' +import dynamic from 'next/dynamic' +import SmartLink from '@/components/SmartLink' +import { useRouter } from 'next/router' +import { createContext, useContext, useEffect, useRef } from 'react' +import BlogPostBar from './components/BlogPostBar' +import CONFIG from './config' +import { Style } from './style' +import Catalog from './components/Catalog' + +const AlgoliaSearchModal = dynamic( + () => import('@/components/AlgoliaSearchModal'), + { ssr: false } +) + +// 主题组件 + +const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), { + ssr: false +}) +const ArticleLock = dynamic(() => import('./components/ArticleLock'), { + ssr: false +}) +const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), { + ssr: false +}) +const Comment = dynamic(() => import('@/components/Comment'), { ssr: false }) +const ArticleAround = dynamic(() => import('./components/ArticleAround'), { + ssr: false +}) +const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false }) +const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false }) +const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), { + ssr: false +}) +const Footer = dynamic(() => import('./components/Footer'), { ssr: false }) +const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false }) +const BlogListPage = dynamic(() => import('./components/BlogListPage'), { + ssr: false +}) +const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), { + ssr: false +}) + +// 主题全局状态 +const ThemeGlobalSimple = createContext() +export const useSimpleGlobal = () => useContext(ThemeGlobalSimple) + +/** + * 基础布局 + * + * @param {*} props + * @returns + */ +const LayoutBase = props => { + const { children } = props + const { onLoading, fullWidth } = useGlobal() + // const onLoading = true + const searchModal = useRef(null) + + return ( + +
+ + ) +} + +export { Style }