From 463b35639a0119c7822c6ee2bc7ce695b9bf93ab Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Mon, 10 Jul 2023 23:23:09 +0800 Subject: [PATCH 001/136] theme-plog --- components/FullScreenButton.js | 48 ++++ themes/hexo/components/Header.js | 2 +- themes/matery/components/Hero.js | 2 +- themes/plog/components/Announcement.js | 18 ++ themes/plog/components/ArticleFooter.js | 34 +++ themes/plog/components/ArticleInfo.js | 59 ++++ themes/plog/components/ArticleLock.js | 53 ++++ themes/plog/components/BlogArchiveItem.js | 41 +++ themes/plog/components/BlogListBar.js | 39 +++ themes/plog/components/BlogListPage.js | 50 ++++ themes/plog/components/BlogListScroll.js | 82 ++++++ themes/plog/components/BlogPost.js | 23 ++ themes/plog/components/BottomNav.js | 88 ++++++ .../plog/components/ExampleRecentComments.js | 35 +++ themes/plog/components/Footer.js | 34 +++ themes/plog/components/JumpToTopButton.js | 19 ++ themes/plog/components/MenuItemCollapse.js | 55 ++++ themes/plog/components/MenuItemDrop.js | 43 +++ themes/plog/components/Nav.js | 93 +++++++ themes/plog/components/PlogModal.js | 28 ++ themes/plog/components/SearchInput.js | 87 ++++++ themes/plog/components/SearchNavBar.js | 17 ++ themes/plog/components/SideBar.js | 65 +++++ themes/plog/components/SvgIcon.js | 29 ++ themes/plog/components/TagItem.js | 13 + themes/plog/components/Tags.js | 38 +++ themes/plog/components/Title.js | 19 ++ themes/plog/config.js | 13 + themes/plog/index.js | 262 ++++++++++++++++++ 29 files changed, 1387 insertions(+), 2 deletions(-) create mode 100644 components/FullScreenButton.js create mode 100644 themes/plog/components/Announcement.js create mode 100644 themes/plog/components/ArticleFooter.js create mode 100644 themes/plog/components/ArticleInfo.js create mode 100644 themes/plog/components/ArticleLock.js create mode 100644 themes/plog/components/BlogArchiveItem.js create mode 100644 themes/plog/components/BlogListBar.js create mode 100644 themes/plog/components/BlogListPage.js create mode 100644 themes/plog/components/BlogListScroll.js create mode 100644 themes/plog/components/BlogPost.js create mode 100644 themes/plog/components/BottomNav.js create mode 100644 themes/plog/components/ExampleRecentComments.js create mode 100644 themes/plog/components/Footer.js create mode 100644 themes/plog/components/JumpToTopButton.js create mode 100644 themes/plog/components/MenuItemCollapse.js create mode 100644 themes/plog/components/MenuItemDrop.js create mode 100644 themes/plog/components/Nav.js create mode 100644 themes/plog/components/PlogModal.js create mode 100644 themes/plog/components/SearchInput.js create mode 100644 themes/plog/components/SearchNavBar.js create mode 100644 themes/plog/components/SideBar.js create mode 100644 themes/plog/components/SvgIcon.js create mode 100644 themes/plog/components/TagItem.js create mode 100644 themes/plog/components/Tags.js create mode 100644 themes/plog/components/Title.js create mode 100644 themes/plog/config.js create mode 100644 themes/plog/index.js diff --git a/components/FullScreenButton.js b/components/FullScreenButton.js new file mode 100644 index 00000000..053de666 --- /dev/null +++ b/components/FullScreenButton.js @@ -0,0 +1,48 @@ +import { isBrowser } from '@/lib/utils' +import React, { useState } from 'react' + +/** + * 全屏按钮 + * @returns + */ +const FullScreenButton = () => { + const [isFullScreen, setIsFullScreen] = useState(false) + + const handleFullScreenClick = () => { + if (!isBrowser()) { + return + } + const element = document.documentElement + if (!isFullScreen) { + if (element.requestFullscreen) { + element.requestFullscreen() + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen() + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen() + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen() + } + setIsFullScreen(true) + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } else if (document.msExitFullscreen) { + document.msExitFullscreen() + } + setIsFullScreen(false) + } + } + + return ( + + ) +} + +export default FullScreenButton diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Header.js index f64263ab..76e86c25 100644 --- a/themes/hexo/components/Header.js +++ b/themes/hexo/components/Header.js @@ -72,7 +72,7 @@ const Hero = props => { -
diff --git a/themes/matery/components/Hero.js b/themes/matery/components/Hero.js index 412f9cf1..6bcd445f 100644 --- a/themes/matery/components/Hero.js +++ b/themes/matery/components/Hero.js @@ -64,7 +64,7 @@ const Hero = props => {
-
diff --git a/themes/plog/components/Announcement.js b/themes/plog/components/Announcement.js new file mode 100644 index 00000000..088c412e --- /dev/null +++ b/themes/plog/components/Announcement.js @@ -0,0 +1,18 @@ +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +const Announcement = ({ notice, className }) => { + if (notice?.blockMap) { + return
+
+ {notice && (
+ +
)} +
+
+ } else { + return null + } +} +export default Announcement diff --git a/themes/plog/components/ArticleFooter.js b/themes/plog/components/ArticleFooter.js new file mode 100644 index 00000000..87cf7baf --- /dev/null +++ b/themes/plog/components/ArticleFooter.js @@ -0,0 +1,34 @@ +import BLOG from '@/blog.config' +import { useRouter } from 'next/router' +import { useGlobal } from '@/lib/global' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleFooter = props => { + const router = useRouter() + const { locale } = useGlobal() + + return
+ + + + + + +
+} diff --git a/themes/plog/components/ArticleInfo.js b/themes/plog/components/ArticleInfo.js new file mode 100644 index 00000000..fc6e70fc --- /dev/null +++ b/themes/plog/components/ArticleInfo.js @@ -0,0 +1,59 @@ + +import formatDate from '@/lib/formatDate' +import Image from 'next/image' +import BLOG from '@/blog.config' +import TagItem from './TagItem' +import md5 from 'js-md5' + +export const ArticleInfo = (props) => { + const { post } = props + + const emailHash = md5(BLOG.CONTACT_EMAIL) + + return
+
+ +
+ {post?.title} +
+ + {post?.type !== 'Page' && <> + + } + +
+ +
+} diff --git a/themes/plog/components/ArticleLock.js b/themes/plog/components/ArticleLock.js new file mode 100644 index 00000000..8bf57e52 --- /dev/null +++ b/themes/plog/components/ArticleLock.js @@ -0,0 +1,53 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleLock = props => { + const { validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
${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/themes/plog/components/BlogArchiveItem.js b/themes/plog/components/BlogArchiveItem.js new file mode 100644 index 00000000..3b16fc3b --- /dev/null +++ b/themes/plog/components/BlogArchiveItem.js @@ -0,0 +1,41 @@ +import BLOG from '@/blog.config' +import Link from 'next/link' + +/** + * 归档分组文章 + * @param {*} param0 + * @returns + */ +export default function BlogArchiveItem({ archiveTitle, archivePosts }) { + return ( +
+
+ {archiveTitle} +
+ +
    + {archivePosts[archiveTitle].map(post => ( +
  • +
    + + {post.date?.start_date} + {' '} +   + + + {post.title} + + +
    +
  • + ))} +
+
+ ) +} diff --git a/themes/plog/components/BlogListBar.js b/themes/plog/components/BlogListBar.js new file mode 100644 index 00000000..69076937 --- /dev/null +++ b/themes/plog/components/BlogListBar.js @@ -0,0 +1,39 @@ +import Tags from './Tags' + +export default function BlogListBar(props) { + const { tag, setFilterKey } = props + const handleSearchChange = (val) => { + setFilterKey(val) + } + if (tag) { + return (
+
+ handleSearchChange(e.target.value)} + /> + + + +
+ +
) + } else { + return <> + } +} diff --git a/themes/plog/components/BlogListPage.js b/themes/plog/components/BlogListPage.js new file mode 100644 index 00000000..9dbf6bd0 --- /dev/null +++ b/themes/plog/components/BlogListPage.js @@ -0,0 +1,50 @@ + +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import { useRouter } from 'next/router' +import Link from 'next/link' +import BlogPost from './BlogPost' + +export const BlogListPage = props => { + const { page = 1, posts, postCount } = props + const { locale } = useGlobal() + const router = useRouter() + const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) + const currentPage = +page + + const showPrev = currentPage > 1 + const showNext = page < totalPage + const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + + return ( +
+ +
+ {posts?.map(post => ( + + ))} +
+ +
+ + + + + + + + + + +
+
+ ) +} diff --git a/themes/plog/components/BlogListScroll.js b/themes/plog/components/BlogListScroll.js new file mode 100644 index 00000000..157e80e4 --- /dev/null +++ b/themes/plog/components/BlogListScroll.js @@ -0,0 +1,82 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import React from 'react' +import throttle from 'lodash.throttle' + +export const BlogListScroll = props => { + const { posts } = props + const { locale } = useGlobal() + + const [page, updatePage] = React.useState(1) + + let hasMore = false + const postsToShow = posts + ? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page) + : [] + + if (posts) { + const totalCount = posts.length + hasMore = page * BLOG.POSTS_PER_PAGE < totalCount + } + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + const targetRef = React.useRef(null) + + // 监听滚动自动分页加载 + const scrollTrigger = React.useCallback(throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500)) + + React.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/themes/plog/components/BlogPost.js b/themes/plog/components/BlogPost.js new file mode 100644 index 00000000..fc7f710a --- /dev/null +++ b/themes/plog/components/BlogPost.js @@ -0,0 +1,23 @@ + +/** + * 博客照片卡牌 + * @param {*} props + * @returns + */ +const BlogPost = (props) => { + const { post, siteInfo } = props + const pageThumbnail = post?.pageCoverThumbnail || siteInfo?.pageCover + console.log('缩略图', pageThumbnail, siteInfo) + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +

+ {post?.title} +

+
+ + ) +} + +export default BlogPost diff --git a/themes/plog/components/BottomNav.js b/themes/plog/components/BottomNav.js new file mode 100644 index 00000000..186f2741 --- /dev/null +++ b/themes/plog/components/BottomNav.js @@ -0,0 +1,88 @@ +import Link from 'next/link' +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { SvgIcon } from './SvgIcon' +import { MenuItemDrop } from './MenuItemDrop' +import FullScreenButton from '@/components/FullScreenButton' + +/** + * 桌面端底部导航 + * @param {*} props + * @returns + */ +const BottomNav = props => { + const { navBarTitle, siteInfo } = props + + return <> + + +} + +/** + * 菜单 + * @param {*} props + * @returns + */ +const MenuList = props => { + const { customMenu, customNav } = props + + const { locale } = useGlobal() + let links = [ + { id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' }, + { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }, + { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( +
+
    + {links?.map(link => )} +
  • + +
  • +
+
+ ) +} + +export default BottomNav diff --git a/themes/plog/components/ExampleRecentComments.js b/themes/plog/components/ExampleRecentComments.js new file mode 100644 index 00000000..b1555c1d --- /dev/null +++ b/themes/plog/components/ExampleRecentComments.js @@ -0,0 +1,35 @@ +import React from 'react' +import BLOG from '@/blog.config' +import Link from 'next/link' +import { RecentComments } from '@waline/client' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const ExampleRecentComments = (props) => { + const [comments, updateComments] = React.useState([]) + const [onLoading, changeLoading] = React.useState(true) + React.useEffect(() => { + RecentComments({ + serverURL: BLOG.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/themes/plog/components/Footer.js b/themes/plog/components/Footer.js new file mode 100644 index 00000000..e2b296b5 --- /dev/null +++ b/themes/plog/components/Footer.js @@ -0,0 +1,34 @@ +import BLOG from '@/blog.config' +import DarkModeButton from '@/components/DarkModeButton' +import Vercel from '@/components/Vercel' + +export const Footer = (props) => { + const d = new Date() + const currentYear = d.getFullYear() + const { post } = props + const fullWidth = post?.fullWidth ?? false + + const copyrightDate = (function() { + if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) { + return BLOG.SINCE + '-' + currentYear + } + return currentYear + })() + + return
+ +
+
+
+

+ © {BLOG.AUTHOR} {copyrightDate} +

+ +
+
+
+} diff --git a/themes/plog/components/JumpToTopButton.js b/themes/plog/components/JumpToTopButton.js new file mode 100644 index 00000000..30e684a8 --- /dev/null +++ b/themes/plog/components/JumpToTopButton.js @@ -0,0 +1,19 @@ +import { useGlobal } from '@/lib/global' +import React from 'react' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = () => { + const { locale } = useGlobal() + return
window.scrollTo({ top: 0, behavior: 'smooth' })} + > +
+} + +export default JumpToTopButton diff --git a/themes/plog/components/MenuItemCollapse.js b/themes/plog/components/MenuItemCollapse.js new file mode 100644 index 00000000..323514a9 --- /dev/null +++ b/themes/plog/components/MenuItemCollapse.js @@ -0,0 +1,55 @@ +import Collapse from '@/components/Collapse' +import Link from 'next/link' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = (props) => { + const { link } = props + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + const [isOpen, changeIsOpen] = useState(false) + + const toggleShow = () => { + changeShow(!show) + } + + const toggleOpenSubMenu = () => { + changeIsOpen(!isOpen) + } + + if (!link || !link.show) { + return null + } + + return <> +
+ {!hasSubMenu && + {link?.icon && }{link?.name} + } + {hasSubMenu &&
+ {link?.icon && }{link?.name} + +
} +
+ + {/* 折叠子菜单 */} + {hasSubMenu && + {link.subMenus.map(sLink => { + return
+ + {sLink.title} + +
+ })} +
} + +} diff --git a/themes/plog/components/MenuItemDrop.js b/themes/plog/components/MenuItemDrop.js new file mode 100644 index 00000000..d1448a18 --- /dev/null +++ b/themes/plog/components/MenuItemDrop.js @@ -0,0 +1,43 @@ +import Link from 'next/link' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + if (!link || !link.show) { + return null + } + + const hasSubMenu = link?.subMenus?.length > 0 + + return
  • +
    changeShow(true)} onMouseOut={() => changeShow(false)}> + {!hasSubMenu && +
    + + {link?.icon && } {link?.name} + +
    + } + + {hasSubMenu && +
    + {link?.icon && } {link?.name} + +
    + } + + {/* 子菜单 */} + {hasSubMenu &&
      + {link.subMenus.map(sLink => { + return
    • + + {link?.icon &&   }{sLink.title} + +
    • + })} +
    } + +
    + +
  • +} diff --git a/themes/plog/components/Nav.js b/themes/plog/components/Nav.js new file mode 100644 index 00000000..52d2a09e --- /dev/null +++ b/themes/plog/components/Nav.js @@ -0,0 +1,93 @@ +import { useRef, useState } from 'react' +import Link from 'next/link' +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { SvgIcon } from './SvgIcon' +import { MenuItemDrop } from './MenuItemDrop' +import Collapse from '@/components/Collapse' +import { MenuItemCollapse } from './MenuItemCollapse' + +const Nav = props => { + const { navBarTitle, fullWidth, siteInfo } = props + return
    + +
    +} + +const NavBar = props => { + const { customMenu, customNav } = props + const [isOpen, changeOpen] = useState(false) + const toggleOpen = () => { + changeOpen(!isOpen) + } + const collapseRef = useRef(null) + + const { locale } = useGlobal() + let links = [ + { id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' }, + { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }, + { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( +
    +
      + {links?.map(link => )} +
    +
    + +
    + {links?.map(link => collapseRef.current?.updateCollapseHeight(param)}/>)} +
    +
    +
    +
    + ) +} + +export default Nav diff --git a/themes/plog/components/PlogModal.js b/themes/plog/components/PlogModal.js new file mode 100644 index 00000000..f08938f2 --- /dev/null +++ b/themes/plog/components/PlogModal.js @@ -0,0 +1,28 @@ +import { useState } from 'react' +import { Dialog } from '@headlessui/react' + +/** + * 图片弹出模态框 + */ +export default function PlogModal(props) { + const [isOpen, setIsOpen] = useState(true) + + return ( + setIsOpen(false)}> + + Deactivate account + + This will permanently deactivate your account + + +

    + Are you sure you want to deactivate your account? All of your data + will be permanently removed. This action cannot be undone. +

    + + + +
    +
    + ) +} diff --git a/themes/plog/components/SearchInput.js b/themes/plog/components/SearchInput.js new file mode 100644 index 00000000..a6affd9c --- /dev/null +++ b/themes/plog/components/SearchInput.js @@ -0,0 +1,87 @@ +import { useRouter } from 'next/router' +import { useGlobal } from '@/lib/global' +import { useImperativeHandle, useRef, useState } from 'react' + +let lock = false + +const SearchInput = ({ currentTag, currentSearch, cRef }) => { + const { locale } = useGlobal() + const router = useRouter() + const searchInputRef = useRef(null) + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + router.push({ pathname: '/search/' + key }).then(r => { + // console.log('搜索', key) + }) + } else { + router.push({ pathname: '/' }).then(r => { + }) + } + } + const handleKeyUp = (e) => { + if (e.keyCode === 13) { // 回车 + handleSearch(searchInputRef.current.value) + } else if (e.keyCode === 27) { // ESC + cleanSearch() + } + } + const cleanSearch = () => { + searchInputRef.current.value = '' + setShowClean(false) + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + const [showClean, setShowClean] = useState(false) + const updateSearchKey = (val) => { + if (lock) { + return + } + searchInputRef.current.value = val + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + + return
    + updateSearchKey(e.target.value)} + defaultValue={currentSearch || ''} + /> + +
    + +
    + + {(showClean && +
    + +
    + )} +
    +} + +export default SearchInput diff --git a/themes/plog/components/SearchNavBar.js b/themes/plog/components/SearchNavBar.js new file mode 100644 index 00000000..79557cff --- /dev/null +++ b/themes/plog/components/SearchNavBar.js @@ -0,0 +1,17 @@ +import SearchInput from './SearchInput' +import Tags from './Tags' + +/** + * 搜索页面上方嵌入内容 + * @param {*} props + * @returns + */ +export default function SearchNavBar(props) { + return (<> +
    + +
    + + + ) +} diff --git a/themes/plog/components/SideBar.js b/themes/plog/components/SideBar.js new file mode 100644 index 00000000..986063b0 --- /dev/null +++ b/themes/plog/components/SideBar.js @@ -0,0 +1,65 @@ +import BLOG from '@/blog.config' +import Live2D from '@/components/Live2D' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import dynamic from 'next/dynamic' +const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments')) + +export const SideBar = (props) => { + const { locale } = useGlobal() + const { latestPosts, categories } = props + return ( +
    + + + + + + {BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT && } + + + +
    + ); +} diff --git a/themes/plog/components/SvgIcon.js b/themes/plog/components/SvgIcon.js new file mode 100644 index 00000000..1ae326ca --- /dev/null +++ b/themes/plog/components/SvgIcon.js @@ -0,0 +1,29 @@ +export const SvgIcon = () => { + return + + + + + + + + + +} diff --git a/themes/plog/components/TagItem.js b/themes/plog/components/TagItem.js new file mode 100644 index 00000000..6c385b9b --- /dev/null +++ b/themes/plog/components/TagItem.js @@ -0,0 +1,13 @@ +import Link from 'next/link' + +const TagItem = ({ tag }) => ( + ( + +

    + {tag} +

    + + ) +) + +export default TagItem diff --git a/themes/plog/components/Tags.js b/themes/plog/components/Tags.js new file mode 100644 index 00000000..bdab3ee5 --- /dev/null +++ b/themes/plog/components/Tags.js @@ -0,0 +1,38 @@ +import Link from 'next/link' + +const Tags = props => { + const { tagOptions, tag } = props + const currentTag = tag + if (!tagOptions) return null + return ( +
    +
      + {Object.keys(tagOptions).map(key => { + const tag = tagOptions[key] + const selected = tag.name === currentTag + return ( +
    • + + + {`${tag.name} (${tag.count})`} + + +
    • + ) + })} +
    +
    + ) +} + +export default Tags diff --git a/themes/plog/components/Title.js b/themes/plog/components/Title.js new file mode 100644 index 00000000..e57e2347 --- /dev/null +++ b/themes/plog/components/Title.js @@ -0,0 +1,19 @@ +import BLOG from '@/blog.config' + +/** + * 标题栏 + * @param {*} props + * @returns + */ +export const Title = (props) => { + const { siteInfo, post } = props + const title = post?.title || siteInfo?.description + const description = post?.description || BLOG.AUTHOR + + return
    +

    {title}

    +

    + {description} +

    +
    +} diff --git a/themes/plog/config.js b/themes/plog/config.js new file mode 100644 index 00000000..75f2e597 --- /dev/null +++ b/themes/plog/config.js @@ -0,0 +1,13 @@ +const CONFIG = { + + // 菜单配置 + MENU_CATEGORY: false, // 显示分类 + MENU_TAG: true, // 显示标签 + MENU_ARCHIVE: false, // 显示归档 + MENU_SEARCH: true, // 显示搜索 + MENU_RSS: false, // 显示订阅 + + NAV_NOTION_ICON: true // 是否读取Notion图标作为站点头像 + +} +export default CONFIG diff --git a/themes/plog/index.js b/themes/plog/index.js new file mode 100644 index 00000000..c7da235d --- /dev/null +++ b/themes/plog/index.js @@ -0,0 +1,262 @@ +import CONFIG from './config' +import CommonHead from '@/components/CommonHead' +import React, { useEffect } from 'react' +import Nav from './components/Nav' +import { useGlobal } from '@/lib/global' + +import BLOG from '@/blog.config' +import { BlogListPage } from './components/BlogListPage' +import { BlogListScroll } from './components/BlogListScroll' + +import { useRouter } from 'next/router' + +import Mark from 'mark.js' +import { isBrowser } from '@/lib/utils' +import SearchNavBar from './components/SearchNavBar' +import BlogArchiveItem from './components/BlogArchiveItem' +import { ArticleLock } from './components/ArticleLock' +import NotionPage from '@/components/NotionPage' +import { ArticleInfo } from './components/ArticleInfo' +import Comment from '@/components/Comment' +import { ArticleFooter } from './components/ArticleFooter' +import ShareBar from '@/components/ShareBar' +import Link from 'next/link' +import { Transition } from '@headlessui/react' +import BottomNav from './components/BottomNav' +import { saveDarkModeToCookies } from '@/themes/theme' + +/** + * 基础布局 采用左右两侧布局,移动端使用顶部导航栏 + + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, meta, topSlot } = props + const { onLoading, updateDarkMode } = useGlobal() + + // 用户手动设置主题 + const setDarkMode = () => { + saveDarkModeToCookies(true) + updateDarkMode(true) + const htmlElement = document.getElementsByTagName('html')[0] + htmlElement.classList?.remove('light') + htmlElement.classList?.add('dark') + } + + // plog主题默认 深色模式 + useEffect(() => { + setTimeout(() => { + setDarkMode() + }, 100) + }, []) + + return ( +
    + {/* SEO相关 */} + + + {/* 移动端顶部导航栏 */} +
    + ) +} + +/** + * 首页 + * 首页是个博客列表,加上顶部嵌入一个公告 + * @param {*} props + * @returns + */ +const LayoutIndex = props => { + return ( + + ) +} + +/** + * 博客列表 + * @param {*} props + * @returns + */ +const LayoutPostList = props => { + return ( + + {BLOG.POST_LIST_STYLE === 'page' ? : } + + ) +} + +/** + * 搜索 + * 页面是博客列表,上方嵌入一个搜索引导条 + * @param {*} props + * @returns + */ +const LayoutSearch = props => { + const { keyword } = props + const router = useRouter() + + useEffect(() => { + setTimeout(() => { + const container = isBrowser() && document.getElementById('posts-wrapper') + if (container && container.innerHTML) { + const re = new RegExp(keyword, 'gim') + const instance = new Mark(container) + instance.markRegExp(re, { + element: 'span', + className: 'text-red-500 border-b border-dashed' + }) + } + }, 100) + }, [router.events]) + + return } /> +} + +/** + * 归档 + * @param {*} props + * @returns + */ +const LayoutArchive = props => { + const { archivePosts } = props + return ( + +
    + {Object.keys(archivePosts).map(archiveTitle => )} +
    +
    + ) +} + +/** + * 文章详情 + * @param {*} props + * @returns + */ +const LayoutSlug = props => { + const { post, lock, validPassword } = props + + return ( + + + {lock && } + + {!lock &&
    + <> + + + + + + +
    } + +
    + ) +} + +/** + * 404 页面 + * @param {*} props + * @returns + */ +const Layout404 = (props) => { + return + 404 Not found. + +} + +/** + * 文章分类列表 + * @param {*} props + * @returns + */ +const LayoutCategoryIndex = (props) => { + const { categoryOptions } = props + + return ( + +
    + {categoryOptions?.map(category => { + return ( + +
    + {category.name}({category.count}) +
    + + ) + })} +
    +
    + ) +} + +/** + * 文章标签列表 + * @param {*} props + * @returns + */ +const LayoutTagIndex = (props) => { + const { tagOptions } = props + return ( + +
    +
    + {tagOptions.map(tag => { + return ( +
    + +
    {tag.name + (tag.count ? `(${tag.count})` : '')}
    + +
    + ) + })} +
    +
    +
    + ) +} + +export { + CONFIG as THEME_CONFIG, + LayoutIndex, + LayoutSearch, + LayoutArchive, + LayoutSlug, + Layout404, + LayoutPostList, + LayoutCategoryIndex, + LayoutTagIndex +} From 563b130e7730eea29fe47d338cabdff5a27deac4 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Tue, 11 Jul 2023 13:39:53 +0800 Subject: [PATCH 002/136] =?UTF-8?q?plog-=E5=9F=BA=E7=A1=80=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/HeroIcons.js | 8 ++- lib/notion/mapImage.js | 7 ++- styles/notion.css | 8 --- themes/plog/components/BlogListPage.js | 2 +- themes/plog/components/BlogPost.js | 28 +++++++-- themes/plog/components/BottomNav.js | 2 +- themes/plog/components/Modal.js | 83 ++++++++++++++++++++++++++ themes/plog/index.js | 16 ++++- 8 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 themes/plog/components/Modal.js diff --git a/components/HeroIcons.js b/components/HeroIcons.js index b4283aaf..8da9fbfa 100644 --- a/components/HeroIcons.js +++ b/components/HeroIcons.js @@ -26,4 +26,10 @@ const User = ({ className }) => { } -export { Moon, Sun, Home, User } + +const ArrowPath = ({ className }) => { + return + + +} +export { Moon, Sun, Home, User, ArrowPath } diff --git a/lib/notion/mapImage.js b/lib/notion/mapImage.js index 2e917b93..8fbfbb1c 100644 --- a/lib/notion/mapImage.js +++ b/lib/notion/mapImage.js @@ -6,7 +6,7 @@ import BLOG from '@/blog.config' * 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸 * @param {*} image */ -const compressImage = (image, width = 300) => { +const compressImage = (image, width = 400, quality = 50, fmt = 'webp') => { if (!image) { return null } @@ -20,11 +20,12 @@ const compressImage = (image, width = 300) => { // 获取URL参数 const params = new URLSearchParams(urlObj.search) // 将q参数的值替换 - params.set('q', '50') + params.set('q', quality) // 尺寸 params.set('width', width) // 格式 - params.set('fmt', 'webp') + params.set('fmt', fmt) + params.set('fm', fmt) // 生成新的URL urlObj.search = params.toString() return urlObj.toString() diff --git a/styles/notion.css b/styles/notion.css index 65345f6d..f2f437f0 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -1950,14 +1950,6 @@ svg + .notion-page-title-text { @apply bg-blue-500 text-gray-50 !important; } -.dark img{ - @apply opacity-80 -} - -.dark #live2d { - @apply opacity-80 -} - /* https://github.com/kchen0x */ .notion-quote { display: block; diff --git a/themes/plog/components/BlogListPage.js b/themes/plog/components/BlogListPage.js index 9dbf6bd0..f8455ae7 100644 --- a/themes/plog/components/BlogListPage.js +++ b/themes/plog/components/BlogListPage.js @@ -19,7 +19,7 @@ export const BlogListPage = props => { return (
    -
    +
    {posts?.map(post => ( ))} diff --git a/themes/plog/components/BlogPost.js b/themes/plog/components/BlogPost.js index fc7f710a..4f7c9bce 100644 --- a/themes/plog/components/BlogPost.js +++ b/themes/plog/components/BlogPost.js @@ -1,3 +1,6 @@ +import { compressImage } from '@/lib/notion/mapImage' +import Link from 'next/link' +import { usePlogGlobal } from '..' /** * 博客照片卡牌 @@ -6,15 +9,30 @@ */ const BlogPost = (props) => { const { post, siteInfo } = props - const pageThumbnail = post?.pageCoverThumbnail || siteInfo?.pageCover - console.log('缩略图', pageThumbnail, siteInfo) + const pageThumbnail = compressImage(post?.pageCoverThumbnail || siteInfo?.pageCover, 800, 80) + const { setModalContent, setShowModal } = usePlogGlobal() + const handleClick = () => { + setShowModal(true) + setModalContent(post) + } return ( -
    +
    {/* eslint-disable-next-line @next/next/no-img-element */} - -

    + +

    {post?.title}

    + {post?.category &&
    + + {post?.category} + +
    }
    ) diff --git a/themes/plog/components/BottomNav.js b/themes/plog/components/BottomNav.js index 186f2741..5567e3dc 100644 --- a/themes/plog/components/BottomNav.js +++ b/themes/plog/components/BottomNav.js @@ -15,7 +15,7 @@ const BottomNav = props => { const { navBarTitle, siteInfo } = props return <> -
    - {post.type === 'Post' && } + {post?.type === 'Post' && } {/* 评论互动 */}
    diff --git a/themes/hexo/index.js b/themes/hexo/index.js index 3e1d6846..dd4e8aa5 100644 --- a/themes/hexo/index.js +++ b/themes/hexo/index.js @@ -227,7 +227,7 @@ const LayoutSlug = props => { {/* 分享 */} - {post.type === 'Post' && <> + {post?.type === 'Post' && <> diff --git a/themes/medium/components/BlogPostListScroll.js b/themes/medium/components/BlogPostListScroll.js index 59a6bd07..0ed06ae3 100644 --- a/themes/medium/components/BlogPostListScroll.js +++ b/themes/medium/components/BlogPostListScroll.js @@ -20,7 +20,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch }) => { const searchKey = getSearchKey() if (searchKey) { filteredPosts = posts.filter(post => { - const tagContent = post.tags ? post.tags.join(' ') : '' + const tagContent = post?.tags ? post?.tags.join(' ') : '' const searchContent = post.title + post.summary + tagContent return searchContent.toLowerCase().includes(searchKey.toLowerCase()) }) diff --git a/themes/medium/index.js b/themes/medium/index.js index fd102bf1..a0884129 100644 --- a/themes/medium/index.js +++ b/themes/medium/index.js @@ -174,7 +174,7 @@ const LayoutSlug = props => {
    {/* 上一篇下一篇文章 */} - {post.type === 'Post' && } + {post?.type === 'Post' && } {/* 评论区 */} diff --git a/themes/next/components/ArticleDetail.js b/themes/next/components/ArticleDetail.js index b8fa6c6c..fab84eeb 100644 --- a/themes/next/components/ArticleDetail.js +++ b/themes/next/components/ArticleDetail.js @@ -87,10 +87,10 @@ export default function ArticleDetail(props) { {/* 版权声明 */} - {post.type === 'Post' && } + {post?.type === 'Post' && } {/* 推荐文章 */} - {post.type === 'Post' && } + {post?.type === 'Post' && }
    {/* 分类 */} @@ -104,7 +104,7 @@ export default function ArticleDetail(props) { } {/* 标签列表 */} - {post.type === 'Post' && ( + {post?.type === 'Post' && ( <> {post.tagItems && (
    @@ -119,7 +119,7 @@ export default function ArticleDetail(props) { )}
    - {post.type === 'Post' && } + {post?.type === 'Post' && } } {/* 评论互动 */} diff --git a/themes/nobelium/components/ArticleInfo.js b/themes/nobelium/components/ArticleInfo.js index f66eb002..7cc873aa 100644 --- a/themes/nobelium/components/ArticleInfo.js +++ b/themes/nobelium/components/ArticleInfo.js @@ -34,7 +34,7 @@ export const ArticleInfo = (props) => {
    {post?.publishTime}
    - {post.tags && ( + {post?.tags && (
    {post?.tags.map(tag => ( diff --git a/themes/nobelium/index.js b/themes/nobelium/index.js index f0412b92..7462d8f1 100644 --- a/themes/nobelium/index.js +++ b/themes/nobelium/index.js @@ -110,7 +110,7 @@ const LayoutPostList = props => { let filteredBlogPosts = [] if (filterKey && posts) { filteredBlogPosts = posts.filter(post => { - const tagContent = post.tags ? post.tags.join(' ') : '' + const tagContent = post?.tags ? post?.tags.join(' ') : '' const searchContent = post.title + post.summary + tagContent return searchContent.toLowerCase().includes(filterKey.toLowerCase()) }) diff --git a/themes/plog/components/ArticleInfo.js b/themes/plog/components/ArticleInfo.js index fc6e70fc..7cc873aa 100644 --- a/themes/plog/components/ArticleInfo.js +++ b/themes/plog/components/ArticleInfo.js @@ -1,5 +1,4 @@ -import formatDate from '@/lib/formatDate' import Image from 'next/image' import BLOG from '@/blog.config' import TagItem from './TagItem' @@ -33,12 +32,9 @@ export const ArticleInfo = (props) => {  / 
    - {formatDate( - post?.publishTime || post?.createdTime, - BLOG.LANG - )} + {post?.publishTime}
    - {post.tags && ( + {post?.tags && (
    {post?.tags.map(tag => ( diff --git a/themes/simple/components/ArticleInfo.js b/themes/simple/components/ArticleInfo.js index 93921c7c..daee32a1 100644 --- a/themes/simple/components/ArticleInfo.js +++ b/themes/simple/components/ArticleInfo.js @@ -21,7 +21,7 @@ export const ArticleInfo = (props) => { {BLOG.AUTHOR} - {post?.publishTime} {post?.category && - {post?.category}} - {post?.tags && post.tags?.length > 0 && post?.tags.map(t => / {t})} + {post?.tags && post?.tags?.length > 0 && post?.tags.map(t => / {t})}
    )} diff --git a/themes/simple/components/BlogItem.js b/themes/simple/components/BlogItem.js index 8a5561c7..a60bd89e 100644 --- a/themes/simple/components/BlogItem.js +++ b/themes/simple/components/BlogItem.js @@ -32,7 +32,7 @@ export const BlogItem = props => {
    {post.category && {post.category}} - {post.tags && post.tags?.length > 0 && post.tags.map(t => /{t})} + {post?.tags && post?.tags?.length > 0 && post?.tags.map(t => /{t})}
    From 326422cd76817be4cb87b0a01b495961cbed25ec Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Tue, 11 Jul 2023 18:33:18 +0800 Subject: [PATCH 010/136] =?UTF-8?q?fukasawa=20=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/fukasawa/components/BlogCard.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/themes/fukasawa/components/BlogCard.js b/themes/fukasawa/components/BlogCard.js index e60303c2..17db3d88 100644 --- a/themes/fukasawa/components/BlogCard.js +++ b/themes/fukasawa/components/BlogCard.js @@ -15,8 +15,9 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => { return (
    From d58dc93d32157aa28e327973b9f02c3d5e7eac97 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Tue, 11 Jul 2023 18:34:07 +0800 Subject: [PATCH 011/136] fukasawa-post-card --- themes/fukasawa/components/BlogCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/fukasawa/components/BlogCard.js b/themes/fukasawa/components/BlogCard.js index 17db3d88..9074c7c4 100644 --- a/themes/fukasawa/components/BlogCard.js +++ b/themes/fukasawa/components/BlogCard.js @@ -55,7 +55,7 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => { {post.category && {post.category} From d08b83aef9b13b510b482e589db1b66e67fa3020 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Tue, 11 Jul 2023 18:39:47 +0800 Subject: [PATCH 012/136] init-heo --- themes/heo/components/AnalyticsCard.js | 30 ++ themes/heo/components/Announcement.js | 21 ++ themes/heo/components/ArticleAdjacent.js | 33 ++ themes/heo/components/ArticleCopyright.js | 41 +++ themes/heo/components/ArticleLock.js | 51 +++ themes/heo/components/ArticleRecommend.js | 65 ++++ themes/heo/components/BlogPostArchive.js | 49 +++ themes/heo/components/BlogPostCard.js | 49 +++ themes/heo/components/BlogPostCardInfo.js | 93 +++++ themes/heo/components/BlogPostListEmpty.js | 14 + themes/heo/components/BlogPostListPage.js | 34 ++ themes/heo/components/BlogPostListScroll.js | 75 ++++ themes/heo/components/Card.js | 9 + themes/heo/components/Catalog.js | 95 +++++ themes/heo/components/CategoryGroup.js | 31 ++ themes/heo/components/FloatDarkModeButton.js | 31 ++ themes/heo/components/Footer.js | 36 ++ themes/heo/components/Header.js | 82 +++++ themes/heo/components/HeaderArticle.js | 79 +++++ themes/heo/components/HexoRecentComments.js | 47 +++ themes/heo/components/InfoCard.js | 32 ++ themes/heo/components/JumpToCommentButton.js | 29 ++ themes/heo/components/JumpToTopButton.js | 25 ++ themes/heo/components/LatestPostsGroup.js | 72 ++++ themes/heo/components/LoadingCover.js | 8 + themes/heo/components/Logo.js | 15 + themes/heo/components/MenuGroupCard.js | 45 +++ themes/heo/components/MenuItemCollapse.js | 54 +++ themes/heo/components/MenuItemDrop.js | 41 +++ themes/heo/components/MenuListSide.js | 37 ++ themes/heo/components/MenuListTop.js | 37 ++ themes/heo/components/NavButtonGroup.js | 33 ++ themes/heo/components/PaginationNumber.js | 107 ++++++ themes/heo/components/Progress.js | 44 +++ themes/heo/components/RightFloatArea.js | 42 +++ themes/heo/components/SearchDrawer.js | 36 ++ themes/heo/components/SearchInput.js | 106 ++++++ themes/heo/components/SearchNav.js | 70 ++++ themes/heo/components/SideBar.js | 33 ++ themes/heo/components/SideBarDrawer.js | 51 +++ themes/heo/components/SideRight.js | 82 +++++ themes/heo/components/SlotBar.js | 24 ++ themes/heo/components/SocialButton.js | 45 +++ themes/heo/components/TagGroups.js | 27 ++ themes/heo/components/TagItemMini.js | 21 ++ themes/heo/components/TocDrawer.js | 42 +++ themes/heo/components/TocDrawerButton.js | 22 ++ themes/heo/components/TopNav.js | 159 +++++++++ themes/heo/config.js | 36 ++ themes/heo/index.js | 355 +++++++++++++++++++ 50 files changed, 2695 insertions(+) create mode 100644 themes/heo/components/AnalyticsCard.js create mode 100644 themes/heo/components/Announcement.js create mode 100644 themes/heo/components/ArticleAdjacent.js create mode 100644 themes/heo/components/ArticleCopyright.js create mode 100644 themes/heo/components/ArticleLock.js create mode 100644 themes/heo/components/ArticleRecommend.js create mode 100644 themes/heo/components/BlogPostArchive.js create mode 100644 themes/heo/components/BlogPostCard.js create mode 100644 themes/heo/components/BlogPostCardInfo.js create mode 100644 themes/heo/components/BlogPostListEmpty.js create mode 100644 themes/heo/components/BlogPostListPage.js create mode 100644 themes/heo/components/BlogPostListScroll.js create mode 100644 themes/heo/components/Card.js create mode 100644 themes/heo/components/Catalog.js create mode 100644 themes/heo/components/CategoryGroup.js create mode 100644 themes/heo/components/FloatDarkModeButton.js create mode 100644 themes/heo/components/Footer.js create mode 100644 themes/heo/components/Header.js create mode 100644 themes/heo/components/HeaderArticle.js create mode 100644 themes/heo/components/HexoRecentComments.js create mode 100644 themes/heo/components/InfoCard.js create mode 100644 themes/heo/components/JumpToCommentButton.js create mode 100644 themes/heo/components/JumpToTopButton.js create mode 100644 themes/heo/components/LatestPostsGroup.js create mode 100644 themes/heo/components/LoadingCover.js create mode 100644 themes/heo/components/Logo.js create mode 100644 themes/heo/components/MenuGroupCard.js create mode 100644 themes/heo/components/MenuItemCollapse.js create mode 100644 themes/heo/components/MenuItemDrop.js create mode 100644 themes/heo/components/MenuListSide.js create mode 100644 themes/heo/components/MenuListTop.js create mode 100644 themes/heo/components/NavButtonGroup.js create mode 100644 themes/heo/components/PaginationNumber.js create mode 100644 themes/heo/components/Progress.js create mode 100644 themes/heo/components/RightFloatArea.js create mode 100644 themes/heo/components/SearchDrawer.js create mode 100644 themes/heo/components/SearchInput.js create mode 100644 themes/heo/components/SearchNav.js create mode 100644 themes/heo/components/SideBar.js create mode 100644 themes/heo/components/SideBarDrawer.js create mode 100644 themes/heo/components/SideRight.js create mode 100644 themes/heo/components/SlotBar.js create mode 100644 themes/heo/components/SocialButton.js create mode 100644 themes/heo/components/TagGroups.js create mode 100644 themes/heo/components/TagItemMini.js create mode 100644 themes/heo/components/TocDrawer.js create mode 100644 themes/heo/components/TocDrawerButton.js create mode 100644 themes/heo/components/TopNav.js create mode 100644 themes/heo/config.js create mode 100644 themes/heo/index.js diff --git a/themes/heo/components/AnalyticsCard.js b/themes/heo/components/AnalyticsCard.js new file mode 100644 index 00000000..0ee1e1cd --- /dev/null +++ b/themes/heo/components/AnalyticsCard.js @@ -0,0 +1,30 @@ +import Card from './Card' + +export function AnalyticsCard (props) { + const { postCount } = props + return +
    + 统计 +
    +
    +
    +
    +
    文章数:
    +
    {postCount}
    +
    +
    +
    +
    +
    访问量:
    +
    +
    +
    +
    +
    +
    访客数:
    +
    +
    +
    +
    + +} diff --git a/themes/heo/components/Announcement.js b/themes/heo/components/Announcement.js new file mode 100644 index 00000000..695c26a4 --- /dev/null +++ b/themes/heo/components/Announcement.js @@ -0,0 +1,21 @@ +import { useGlobal } from '@/lib/global' +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +const Announcement = ({ post, className }) => { + const { locale } = useGlobal() + if (post?.blockMap) { + return
    +
    +
    {locale.COMMON.ANNOUNCEMENT}
    + {post && (
    + +
    )} +
    +
    + } else { + return <> + } +} +export default Announcement diff --git a/themes/heo/components/ArticleAdjacent.js b/themes/heo/components/ArticleAdjacent.js new file mode 100644 index 00000000..21ca9e32 --- /dev/null +++ b/themes/heo/components/ArticleAdjacent.js @@ -0,0 +1,33 @@ +import Link from 'next/link' +import CONFIG from '../config' + +/** + * 上一篇,下一篇文章 + * @param {prev,next} param0 + * @returns + */ +export default function ArticleAdjacent ({ prev, next }) { + if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) { + return <> + } + return ( +
    + + + {prev.title} + + + + {next.title} + + + +
    + ) +} diff --git a/themes/heo/components/ArticleCopyright.js b/themes/heo/components/ArticleCopyright.js new file mode 100644 index 00000000..4664573c --- /dev/null +++ b/themes/heo/components/ArticleCopyright.js @@ -0,0 +1,41 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import CONFIG from '../config' + +export default function ArticleCopyright () { + if (!CONFIG.ARTICLE_COPYRIGHT) { + return <> + } + const router = useRouter() + const [path, setPath] = useState(BLOG.LINK + router.asPath) + useEffect(() => { + setPath(window.location.href) + }) + + const { locale } = useGlobal() + return ( +
    +
      +
    • + {locale.COMMON.AUTHOR}: + + {BLOG.AUTHOR} + +
    • +
    • + {locale.COMMON.URL}: + + {path} + +
    • +
    • + {locale.COMMON.COPYRIGHT}: + {locale.COMMON.COPYRIGHT_NOTICE} +
    • +
    +
    + ); +} diff --git a/themes/heo/components/ArticleLock.js b/themes/heo/components/ArticleLock.js new file mode 100644 index 00000000..7f1da728 --- /dev/null +++ b/themes/heo/components/ArticleLock.js @@ -0,0 +1,51 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleLock = props => { + const { validPassword } = props + const { locale } = useGlobal() + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
    ${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 bg-gray-100 dark:bg-gray-500'> + +
    +  {locale.COMMON.SUBMIT} +
    +
    +
    +
    +
    +
    +} diff --git a/themes/heo/components/ArticleRecommend.js b/themes/heo/components/ArticleRecommend.js new file mode 100644 index 00000000..e4943cf8 --- /dev/null +++ b/themes/heo/components/ArticleRecommend.js @@ -0,0 +1,65 @@ +import Link from 'next/link' +import CONFIG from '../config' +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' + +/** + * 关联推荐文章 + * @param {prev,next} param0 + * @returns + */ +export default function ArticleRecommend({ recommendPosts, siteInfo }) { + const { locale } = useGlobal() + + if ( + !CONFIG.ARTICLE_RECOMMEND || + !recommendPosts || + recommendPosts.length === 0 + ) { + return <> + } + + return ( +
    +
    +
    + + {locale.COMMON.RELATE_POSTS} +
    +
    +
    + {recommendPosts.map(post => { + const headerImage = post?.pageCoverThumbnail + ? `url("${post.pageCoverThumbnail}")` + : `url("${siteInfo?.pageCover}")` + + return ( + ( + +
    +
    +
    +
    + + {post.date?.start_date} +
    +
    {post.title}
    +
    +
    +
    + + ) + ) + })} +
    +
    + ) +} diff --git a/themes/heo/components/BlogPostArchive.js b/themes/heo/components/BlogPostArchive.js new file mode 100644 index 00000000..08feff4c --- /dev/null +++ b/themes/heo/components/BlogPostArchive.js @@ -0,0 +1,49 @@ +import React from 'react' +import Link from 'next/link' +import BLOG from '@/blog.config' +/** + * 博客归档列表 + * @param posts 所有文章 + * @param archiveTitle 归档标题 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostArchive = ({ posts = [], archiveTitle }) => { + if (!posts || posts.length === 0) { + return <> + } else { + return ( +
    +
    + {archiveTitle} +
    +
      + {posts?.map(post => ( +
    • +
      + {post.date?.start_date}{' '} +   + + + {post.title} + + +
      +
    • + ))} +
    +
    + ) + } +} + +export default BlogPostArchive diff --git a/themes/heo/components/BlogPostCard.js b/themes/heo/components/BlogPostCard.js new file mode 100644 index 00000000..648ce49e --- /dev/null +++ b/themes/heo/components/BlogPostCard.js @@ -0,0 +1,49 @@ +import Link from 'next/link' +import React from 'react' +import CONFIG from '../config' +import { BlogPostCardInfo } from './BlogPostCardInfo' +import BLOG from '@/blog.config' +// import Image from 'next/image' + +const BlogPostCard = ({ index, post, showSummary, siteInfo }) => { + const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap + if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) { + post.pageCover = siteInfo?.pageCoverThumbnail + } + const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail && !showPreview + // const delay = (index % 2) * 200 + + return ( + +
    +
    + + {/* 文字内容 */} + + + {/* 图片封面 */} + {showPageCover && ( +
    + +
    + +
    + )} + +
    + +
    + + ) +} + +export default BlogPostCard diff --git a/themes/heo/components/BlogPostCardInfo.js b/themes/heo/components/BlogPostCardInfo.js new file mode 100644 index 00000000..4b7feee6 --- /dev/null +++ b/themes/heo/components/BlogPostCardInfo.js @@ -0,0 +1,93 @@ +import NotionPage from '@/components/NotionPage' +import Link from 'next/link' +import TagItemMini from './TagItemMini' +import TwikooCommentCount from '@/components/TwikooCommentCount' +import BLOG from '@/blog.config' + +/** + * 博客列表的文字内容 + * @param {*} param0 + * @returns + */ +export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary }) => { + return
    +
    + {/* 标题 */} + + + {post.title} + + + + {/* 分类 */} + { post?.category &&
    + + + + {post.category} + + + + +
    } + + {/* 摘要 */} + {(!showPreview || showSummary) && !post.results && ( +

    + {post.summary} +

    + )} + + {/* 搜索结果 */} + {post.results && ( +

    + {post.results.map(r => ( + {r} + ))} +

    + )} + {/* 预览 */} + {showPreview && ( +
    + +
    + )} + +
    + +
    + {/* 日期标签 */} +
    + {/* 日期 */} + + + + {post?.publishTime || post.lastEditedTime} + + + +
    +
    + {' '} + {post.tagItems?.map(tag => ( + + ))} +
    +
    +
    +
    +
    +} diff --git a/themes/heo/components/BlogPostListEmpty.js b/themes/heo/components/BlogPostListEmpty.js new file mode 100644 index 00000000..5f75c3e7 --- /dev/null +++ b/themes/heo/components/BlogPostListEmpty.js @@ -0,0 +1,14 @@ +import { useGlobal } from '@/lib/global' + +/** + * 空白博客 列表 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListEmpty = ({ currentSearch }) => { + const { locale } = useGlobal() + return
    +
    {locale.COMMON.NO_MORE} {(currentSearch &&
    {currentSearch}
    )}
    +
    +} +export default BlogPostListEmpty diff --git a/themes/heo/components/BlogPostListPage.js b/themes/heo/components/BlogPostListPage.js new file mode 100644 index 00000000..92008f83 --- /dev/null +++ b/themes/heo/components/BlogPostListPage.js @@ -0,0 +1,34 @@ +import BlogPostCard from './BlogPostCard' +import PaginationNumber from './PaginationNumber' +import BLOG from '@/blog.config' +import BlogPostListEmpty from './BlogPostListEmpty' + +/** + * 文章列表分页表格 + * @param page 当前页 + * @param posts 所有文章 + * @param tags 所有标签 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { + const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) + const showPagination = postCount >= BLOG.POSTS_PER_PAGE + if (!posts || posts.length === 0 || page > totalPage) { + return + } else { + return ( +
    + {/* 文章列表 */} +
    + {posts?.map(post => ( + + ))} +
    + {showPagination && } +
    + ) + } +} + +export default BlogPostListPage diff --git a/themes/heo/components/BlogPostListScroll.js b/themes/heo/components/BlogPostListScroll.js new file mode 100644 index 00000000..830e8177 --- /dev/null +++ b/themes/heo/components/BlogPostListScroll.js @@ -0,0 +1,75 @@ +import BLOG from '@/blog.config' +import BlogPostCard from './BlogPostCard' +import BlogPostListEmpty from './BlogPostListEmpty' +import { useGlobal } from '@/lib/global' +import React from 'react' +import CONFIG from '../config' +import { getListByPage } from '@/lib/utils' + +/** + * 博客列表滚动分页 + * @param posts 所有文章 + * @param tags 所有标签 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.POST_LIST_SUMMARY, siteInfo }) => { + const postsPerPage = BLOG.POSTS_PER_PAGE + const [page, updatePage] = React.useState(1) + const postsToShow = getListByPage(posts, page, postsPerPage) + + let hasMore = false + if (posts) { + const totalCount = posts.length + hasMore = page * postsPerPage < totalCount + } + + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + // 监听滚动自动分页加载 + const scrollTrigger = () => { + requestAnimationFrame(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }) + } + + // 监听滚动 + React.useEffect(() => { + window.addEventListener('scroll', scrollTrigger) + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }) + + const targetRef = React.useRef(null) + const { locale } = useGlobal() + + if (!postsToShow || postsToShow.length === 0) { + return + } else { + return
    + + {/* 文章列表 */} +
    + {postsToShow.map(post => ( + + ))} +
    + +
    +
    { handleGetMore() }} + className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200' + > {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`}
    +
    +
    + } +} + +export default BlogPostListScroll diff --git a/themes/heo/components/Card.js b/themes/heo/components/Card.js new file mode 100644 index 00000000..c2db0e49 --- /dev/null +++ b/themes/heo/components/Card.js @@ -0,0 +1,9 @@ +const Card = ({ children, headerSlot, className }) => { + return
    + <>{headerSlot} +
    + {children} +
    +
    +} +export default Card diff --git a/themes/heo/components/Catalog.js b/themes/heo/components/Catalog.js new file mode 100644 index 00000000..980be47b --- /dev/null +++ b/themes/heo/components/Catalog.js @@ -0,0 +1,95 @@ +import React, { useRef } from 'react' +import throttle from 'lodash.throttle' +import { uuidToId } from 'notion-utils' +import Progress from './Progress' +import { useGlobal } from '@/lib/global' + +/** + * 目录导航组件 + * @param toc + * @returns {JSX.Element} + * @constructor + */ +const Catalog = ({ toc }) => { + const { locale } = useGlobal() + // 监听滚动事件 + React.useEffect(() => { + window.addEventListener('scroll', actionSectionScrollSpy) + actionSectionScrollSpy() + return () => { + window.removeEventListener('scroll', actionSectionScrollSpy) + } + }, []) + + // 目录自动滚动 + const tRef = useRef(null) + const tocIds = [] + + // 同步选中目录事件 + const [activeSection, setActiveSection] = React.useState(null) + + const throttleMs = 200 + const actionSectionScrollSpy = React.useCallback(throttle(() => { + const sections = document.getElementsByClassName('notion-h') + let prevBBox = null + let currentSectionId = activeSection + for (let i = 0; i < sections.length; ++i) { + const section = sections[i] + if (!section || !(section instanceof Element)) continue + if (!currentSectionId) { + currentSectionId = section.getAttribute('data-id') + } + const bbox = section.getBoundingClientRect() + const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0 + const offset = Math.max(150, prevHeight / 4) + // GetBoundingClientRect returns values relative to viewport + if (bbox.top - offset < 0) { + currentSectionId = section.getAttribute('data-id') + prevBBox = bbox + continue + } + // No need to continue loop, if last element has been detected + break + } + setActiveSection(currentSectionId) + const index = tocIds.indexOf(currentSectionId) || 0 + tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' }) + }, throttleMs)) + + // 无目录就直接返回空 + if (!toc || toc.length < 1) { + return <> + } + + return
    +
    {locale.COMMON.TABLE_OF_CONTENTS}
    +
    + +
    +
    + + +
    +
    +} + +export default Catalog diff --git a/themes/heo/components/CategoryGroup.js b/themes/heo/components/CategoryGroup.js new file mode 100644 index 00000000..c88df60a --- /dev/null +++ b/themes/heo/components/CategoryGroup.js @@ -0,0 +1,31 @@ +import Link from 'next/link' +import React from 'react' + +const CategoryGroup = ({ currentCategory, categories }) => { + if (!categories) { + return <> + } + return <> +
    + {categories.map(category => { + const selected = currentCategory === category.name + return ( + + +
    {category.name}({category.count})
    + + + ); + })} +
    + ; +} + +export default CategoryGroup diff --git a/themes/heo/components/FloatDarkModeButton.js b/themes/heo/components/FloatDarkModeButton.js new file mode 100644 index 00000000..f693d1f0 --- /dev/null +++ b/themes/heo/components/FloatDarkModeButton.js @@ -0,0 +1,31 @@ +import { useGlobal } from '@/lib/global' +import { saveDarkModeToCookies } from '@/themes/theme' +import CONFIG from '../config' + +export default function FloatDarkModeButton () { + const { isDarkMode, updateDarkMode } = useGlobal() + + if (!CONFIG.WIDGET_DARK_MODE) { + return <> + } + + // 用户手动设置主题 + const handleChangeDarkMode = () => { + const newStatus = !isDarkMode + saveDarkModeToCookies(newStatus) + updateDarkMode(newStatus) + const htmlElement = document.getElementsByTagName('html')[0] + htmlElement.classList?.remove(newStatus ? 'light' : 'dark') + htmlElement.classList?.add(newStatus ? 'dark' : 'light') + } + + return ( +
    + +
    + ) +} diff --git a/themes/heo/components/Footer.js b/themes/heo/components/Footer.js new file mode 100644 index 00000000..f67b53f2 --- /dev/null +++ b/themes/heo/components/Footer.js @@ -0,0 +1,36 @@ +import React from 'react' +import BLOG from '@/blog.config' +// import DarkModeButton from '@/components/DarkModeButton' + +const Footer = ({ title }) => { + const d = new Date() + const currentYear = d.getFullYear() + const copyrightDate = (function() { + if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) { + return BLOG.SINCE + '-' + currentYear + } + return currentYear + })() + + return ( + + ) +} + +export default Footer diff --git a/themes/heo/components/Header.js b/themes/heo/components/Header.js new file mode 100644 index 00000000..76e86c25 --- /dev/null +++ b/themes/heo/components/Header.js @@ -0,0 +1,82 @@ +// import Image from 'next/image' +import { useEffect, useState } from 'react' +import Typed from 'typed.js' +import CONFIG from '../config' +import NavButtonGroup from './NavButtonGroup' +import { useGlobal } from '@/lib/global' +import BLOG from '@/blog.config' + +let wrapperTop = 0 + +/** + * 顶部全屏大图 + * @returns + */ +const Hero = props => { + const [typed, changeType] = useState() + const { siteInfo } = props + const { locale } = useGlobal() + const scrollToWrapper = () => { + window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) + } + useEffect(() => { + updateHeaderHeight() + + if (!typed && window && document.getElementById('typed')) { + changeType( + new Typed('#typed', { + strings: BLOG.GREETING_WORDS.split(','), + typeSpeed: 200, + backSpeed: 100, + backDelay: 400, + showCursor: true, + smartBackspace: true + }) + ) + } + + window.addEventListener('resize', updateHeaderHeight) + return () => { + window.removeEventListener('resize', updateHeaderHeight) + } + }) + + function updateHeaderHeight() { + requestAnimationFrame(() => { + const wrapperElement = document.getElementById('wrapper') + wrapperTop = wrapperElement?.offsetTop + }) + } + + return ( + + ) +} + +export default Hero diff --git a/themes/heo/components/HeaderArticle.js b/themes/heo/components/HeaderArticle.js new file mode 100644 index 00000000..b5384a1f --- /dev/null +++ b/themes/heo/components/HeaderArticle.js @@ -0,0 +1,79 @@ +import Link from 'next/link' +import TagItemMini from './TagItemMini' +import { useGlobal } from '@/lib/global' +import BLOG from '@/blog.config' +import NotionIcon from '@/components/NotionIcon' + +export default function HeaderArticle({ post, siteInfo }) { + const { locale } = useGlobal() + + if (!post) { + return <> + } + const headerImage = post?.pageCover ? `url("${post.pageCover}")` : `url("${siteInfo?.pageCover}")` + + return ( + + ) +} diff --git a/themes/heo/components/HexoRecentComments.js b/themes/heo/components/HexoRecentComments.js new file mode 100644 index 00000000..2ebf00c8 --- /dev/null +++ b/themes/heo/components/HexoRecentComments.js @@ -0,0 +1,47 @@ +import React from 'react' +import BLOG from '@/blog.config' +import Card from '@/themes/hexo/components/Card' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { RecentComments } from '@waline/client' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const HexoRecentComments = (props) => { + const [comments, updateComments] = React.useState([]) + const { locale } = useGlobal() + const [onLoading, changeLoading] = React.useState(true) + React.useEffect(() => { + RecentComments({ + serverURL: BLOG.COMMENT_WALINE_SERVER_URL, + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return ( + +
    + + {locale.COMMON.RECENT_COMMENTS} +
    + + {onLoading &&
    Loading...
    } + {!onLoading && comments && comments.length === 0 &&
    No Comments
    } + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
    +
    +
    + --{comment.nick} +
    +
    )} + + + ) +} + +export default HexoRecentComments diff --git a/themes/heo/components/InfoCard.js b/themes/heo/components/InfoCard.js new file mode 100644 index 00000000..661da999 --- /dev/null +++ b/themes/heo/components/InfoCard.js @@ -0,0 +1,32 @@ +import BLOG from '@/blog.config' +import { useRouter } from 'next/router' +import Card from './Card' +import SocialButton from './SocialButton' +import MenuGroupCard from './MenuGroupCard' + +/** + * 社交信息卡 + * @param {*} props + * @returns + */ +export function InfoCard(props) { + const { className, siteInfo } = props + const router = useRouter() + return ( + +
    { + router.push('/') + }} + > + {/* eslint-disable-next-line @next/next/no-img-element */} + {BLOG.AUTHOR} +
    +
    {BLOG.AUTHOR}
    +
    {BLOG.BIO}
    + + +
    + ) +} diff --git a/themes/heo/components/JumpToCommentButton.js b/themes/heo/components/JumpToCommentButton.js new file mode 100644 index 00000000..fb007712 --- /dev/null +++ b/themes/heo/components/JumpToCommentButton.js @@ -0,0 +1,29 @@ +import React from 'react' +import CONFIG from '../config' + +/** + * 跳转到评论区 + * @returns {JSX.Element} + * @constructor + */ +const JumpToCommentButton = () => { + if (!CONFIG.WIDGET_TO_COMMENT) { + return <> + } + + function navToComment() { + if (document.getElementById('comment')) { + window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' }) + } + // 兼容性不好 + // const commentElement = document.getElementById('comment') + // if (commentElement) { + // commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + } + + return (
    + +
    ) +} + +export default JumpToCommentButton diff --git a/themes/heo/components/JumpToTopButton.js b/themes/heo/components/JumpToTopButton.js new file mode 100644 index 00000000..77313f46 --- /dev/null +++ b/themes/heo/components/JumpToTopButton.js @@ -0,0 +1,25 @@ +import { useGlobal } from '@/lib/global' +import React from 'react' +import CONFIG from '../config' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = ({ showPercent = true, percent }) => { + const { locale } = useGlobal() + + if (!CONFIG.WIDGET_TO_TOP) { + return <> + } + return (
    window.scrollTo({ top: 0, behavior: 'smooth' })} > +
    + {showPercent && (
    {percent}
    )} +
    ) +} + +export default JumpToTopButton diff --git a/themes/heo/components/LatestPostsGroup.js b/themes/heo/components/LatestPostsGroup.js new file mode 100644 index 00000000..22ac3e42 --- /dev/null +++ b/themes/heo/components/LatestPostsGroup.js @@ -0,0 +1,72 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +// import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 最新文章列表 + * @param posts 所有文章数据 + * @param sliceCount 截取展示的数量 默认6 + * @constructor + */ +const LatestPostsGroup = ({ latestPosts, siteInfo }) => { + // 获取当前路径 + const currentPath = useRouter().asPath + const { locale } = useGlobal() + + if (!latestPosts) { + return <> + } + + return <> +
    +
    + + {locale.COMMON.LATEST_POSTS} +
    +
    + {latestPosts.map(post => { + const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}` + + const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover + + return ( + ( + +
    + {/* {post.title} */} + {/* eslint-disable-next-line @next/next/no-img-element */} + +
    +
    +
    +
    {post.title}
    +
    {post.lastEditedTime}
    +
    +
    + + ) + ) + })} + +} +export default LatestPostsGroup diff --git a/themes/heo/components/LoadingCover.js b/themes/heo/components/LoadingCover.js new file mode 100644 index 00000000..c6418fad --- /dev/null +++ b/themes/heo/components/LoadingCover.js @@ -0,0 +1,8 @@ +export default function LoadingCover () { + return (
    +
    + +
    +
    + ) +} diff --git a/themes/heo/components/Logo.js b/themes/heo/components/Logo.js new file mode 100644 index 00000000..2dd392b7 --- /dev/null +++ b/themes/heo/components/Logo.js @@ -0,0 +1,15 @@ +import BLOG from '@/blog.config' +import Link from 'next/link' +import React from 'react' + +const Logo = props => { + const { siteInfo } = props + return ( + +
    +
    {siteInfo?.title || BLOG.TITLE}
    +
    + + ) +} +export default Logo diff --git a/themes/heo/components/MenuGroupCard.js b/themes/heo/components/MenuGroupCard.js new file mode 100644 index 00000000..25dba50c --- /dev/null +++ b/themes/heo/components/MenuGroupCard.js @@ -0,0 +1,45 @@ +import React from 'react' +import Link from 'next/link' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' + +const MenuGroupCard = (props) => { + const { postCount, categoryOptions, tagOptions } = props + const { locale } = useGlobal() + const archiveSlot =
    {postCount}
    + const categorySlot =
    {categoryOptions?.length}
    + const tagSlot =
    {tagOptions?.length}
    + + const links = [ + { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE }, + { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG.MENU_CATEGORY }, + { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG } + ] + + return ( + + ) +} +export default MenuGroupCard diff --git a/themes/heo/components/MenuItemCollapse.js b/themes/heo/components/MenuItemCollapse.js new file mode 100644 index 00000000..3ec10f5e --- /dev/null +++ b/themes/heo/components/MenuItemCollapse.js @@ -0,0 +1,54 @@ +import Collapse from '@/components/Collapse' +import Link from 'next/link' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = ({ link }) => { + 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 => { + return
    + + {link?.icon && } {sLink.title} + +
    + })} +
    } + +} diff --git a/themes/heo/components/MenuItemDrop.js b/themes/heo/components/MenuItemDrop.js new file mode 100644 index 00000000..2dfb6f79 --- /dev/null +++ b/themes/heo/components/MenuItemDrop.js @@ -0,0 +1,41 @@ +import Link from 'next/link' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + if (!link || !link.show) { + return null + } + + return
    changeShow(true)} onMouseOut={() => changeShow(false)} > + + {!hasSubMenu && + + {link?.icon && } {link?.name} + {hasSubMenu && } + } + + {hasSubMenu && <> +
    + {link?.icon && } {link?.name} + +
    + } + + {/* 子菜单 */} + {hasSubMenu &&
      + {link.subMenus.map((sLink, index) => { + return
    • + + {link?.icon &&   }{sLink.title} + +
    • + })} +
    } + +
    +} diff --git a/themes/heo/components/MenuListSide.js b/themes/heo/components/MenuListSide.js new file mode 100644 index 00000000..1a3b2f5b --- /dev/null +++ b/themes/heo/components/MenuListSide.js @@ -0,0 +1,37 @@ +import React from 'react' +import { useGlobal } from '@/lib/global' +import BLOG from '@/blog.config' +import { MenuItemCollapse } from './MenuItemCollapse' +import CONFIG from '../config' + +export const MenuListSide = (props) => { + const { customNav, customMenu } = props + const { locale } = useGlobal() + + let links = [ + { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }, + { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + + if (customNav) { + links = customNav.concat(links) + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( + + ) +} diff --git a/themes/heo/components/MenuListTop.js b/themes/heo/components/MenuListTop.js new file mode 100644 index 00000000..a2fd66ad --- /dev/null +++ b/themes/heo/components/MenuListTop.js @@ -0,0 +1,37 @@ +import React from 'react' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import BLOG from '@/blog.config' +import { MenuItemDrop } from './MenuItemDrop' + +export const MenuListTop = (props) => { + const { customNav, customMenu } = props + const { locale } = useGlobal() + + let links = [ + { id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG.MENU_INDEX }, + { id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE } + // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return (<> + + ) +} diff --git a/themes/heo/components/NavButtonGroup.js b/themes/heo/components/NavButtonGroup.js new file mode 100644 index 00000000..2a3fc898 --- /dev/null +++ b/themes/heo/components/NavButtonGroup.js @@ -0,0 +1,33 @@ + +import React from 'react' +import Link from 'next/link' + +/** + * 首页导航大按钮组件 + * @param {*} props + * @returns + */ +const NavButtonGroup = (props) => { + const { categoryOptions } = props + if (!categoryOptions || categoryOptions.length === 0) { + return <> + } + + return ( + + ) +} +export default NavButtonGroup diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js new file mode 100644 index 00000000..42281b93 --- /dev/null +++ b/themes/heo/components/PaginationNumber.js @@ -0,0 +1,107 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 数字翻页插件 + * @param page 当前页码 + * @param showNext 是否有下一页 + * @returns {JSX.Element} + * @constructor + */ +const PaginationNumber = ({ page, totalPage }) => { + const router = useRouter() + const currentPage = +page + const showNext = page < totalPage + const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + const pages = generatePages(pagePrefix, page, currentPage, totalPage) + + return ( +
    + {/* 上一页 */} + + + + + + + {pages} + + {/* 下一页 */} + + + + + +
    + ) +} + +function getPageElement(page, currentPage, pagePrefix) { + return ( + ( + + {page} + + ) + ) +} + +function generatePages(pagePrefix, page, currentPage, totalPage) { + const pages = [] + const groupCount = 7 // 最多显示页签数 + if (totalPage <= groupCount) { + for (let i = 1; i <= totalPage; i++) { + pages.push(getPageElement(i, page, pagePrefix)) + } + } else { + pages.push(getPageElement(1, page, pagePrefix)) + const dynamicGroupCount = groupCount - 2 + let startPage = currentPage - 2 + if (startPage <= 1) { + startPage = 2 + } + if (startPage + dynamicGroupCount > totalPage) { + startPage = totalPage - dynamicGroupCount + } + if (startPage > 2) { + pages.push(
    ...
    ) + } + + for (let i = 0; i < dynamicGroupCount; i++) { + if (startPage + i < totalPage) { + pages.push(getPageElement(startPage + i, page, pagePrefix)) + } + } + + if (startPage + dynamicGroupCount < totalPage) { + pages.push(
    ...
    ) + } + + pages.push(getPageElement(totalPage, page, pagePrefix)) + } + return pages +} +export default PaginationNumber diff --git a/themes/heo/components/Progress.js b/themes/heo/components/Progress.js new file mode 100644 index 00000000..5b4f9b20 --- /dev/null +++ b/themes/heo/components/Progress.js @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from 'react' +import { isBrowser } from '@/lib/utils' + +/** + * 顶部页面阅读进度条 + * @returns {JSX.Element} + * @constructor + */ +const Progress = ({ targetRef, showPercent = true }) => { + const currentRef = targetRef?.current || targetRef + const [percent, changePercent] = useState(0) + const scrollListener = () => { + const target = currentRef || (isBrowser() && document.getElementById('article-wrapper')) + if (target) { + const clientHeight = target.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + if (per < 0) per = 0 + changePercent(per) + } + } + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, []) + + return ( +
    +
    + {showPercent && ( +
    {percent}%
    + )} +
    +
    + ) +} + +export default Progress diff --git a/themes/heo/components/RightFloatArea.js b/themes/heo/components/RightFloatArea.js new file mode 100644 index 00000000..d7fadce5 --- /dev/null +++ b/themes/heo/components/RightFloatArea.js @@ -0,0 +1,42 @@ +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useState } from 'react' +import FloatDarkModeButton from './FloatDarkModeButton' +import JumpToTopButton from './JumpToTopButton' + +/** + * 悬浮在右下角的按钮,当页面向下滚动100px时会出现 + * @param {*} param0 + * @returns + */ +export default function RightFloatArea({ floatSlot }) { + const [showFloatButton, switchShow] = useState(false) + const scrollListener = useCallback(throttle(() => { + const targetRef = document.getElementById('wrapper') + const clientHeight = targetRef?.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + const shouldShow = scrollY > 100 && per > 0 + + // 右下角显示悬浮按钮 + if (shouldShow !== showFloatButton) { + switchShow(shouldShow) + } + }, 200)) + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, []) + + return ( +
    +
    + + {floatSlot} + +
    +
    + ) +} diff --git a/themes/heo/components/SearchDrawer.js b/themes/heo/components/SearchDrawer.js new file mode 100644 index 00000000..c7ec88a7 --- /dev/null +++ b/themes/heo/components/SearchDrawer.js @@ -0,0 +1,36 @@ +import { Router } from 'next/router' +import { useImperativeHandle, useRef } from 'react' +import SearchInput from './SearchInput' +const SearchDrawer = ({ cRef, slot }) => { + const searchDrawer = useRef() + const searchInputRef = useRef() + useImperativeHandle(cRef, () => { + return { + show: () => { + searchDrawer?.current?.classList?.remove('hidden') + searchInputRef?.current?.focus() + } + } + }) + const hidden = () => { + searchDrawer?.current?.classList?.add('hidden') + } + Router.events.on('routeChangeComplete', (...args) => { + hidden() + }) + return ( +
    +
    +
    + + {slot} +
    +
    + + {/* 背景蒙版 */} +
    +
    + ) +} + +export default SearchDrawer diff --git a/themes/heo/components/SearchInput.js b/themes/heo/components/SearchInput.js new file mode 100644 index 00000000..462c58b3 --- /dev/null +++ b/themes/heo/components/SearchInput.js @@ -0,0 +1,106 @@ +import { useRouter } from 'next/router' +import { useImperativeHandle, useRef, useState } from 'react' +import { useGlobal } from '@/lib/global' +let lock = false + +const SearchInput = props => { + const { currentSearch, cRef, className } = props + const [onLoading, setLoadingState] = useState(false) + const router = useRouter() + const searchInputRef = useRef() + const { locale } = useGlobal() + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + setLoadingState(true) + router.push({ pathname: '/search/' + key }).then(r => { + setLoadingState(false) + }) + // location.href = '/search/' + key + } else { + router.push({ pathname: '/' }).then(r => {}) + } + } + const handleKeyUp = e => { + if (e.keyCode === 13) { + // 回车 + handleSearch(searchInputRef.current.value) + } else if (e.keyCode === 27) { + // ESC + cleanSearch() + } + } + const cleanSearch = () => { + searchInputRef.current.value = '' + } + + const [showClean, setShowClean] = useState(false) + const updateSearchKey = val => { + if (lock) { + return + } + searchInputRef.current.value = val + + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + + return ( +
    + updateSearchKey(e.target.value)} + defaultValue={currentSearch || ''} + /> + +
    + +
    + + {showClean && ( +
    + +
    + )} +
    + ) +} + +export default SearchInput diff --git a/themes/heo/components/SearchNav.js b/themes/heo/components/SearchNav.js new file mode 100644 index 00000000..359f5c8c --- /dev/null +++ b/themes/heo/components/SearchNav.js @@ -0,0 +1,70 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useEffect, useRef } from 'react' +import Card from './Card' +import SearchInput from './SearchInput' +import TagItemMini from './TagItemMini' + +/** + * 搜索页面的导航 + * @param {*} props + * @returns + */ +export default function SearchNav(props) { + const { tagOptions, categoryOptions } = props + const cRef = useRef(null) + const { locale } = useGlobal() + useEffect(() => { + // 自动聚焦到搜索框 + cRef?.current?.focus() + }, []) + + return <> +
    + + {/* 分类 */} + +
    + + {locale.COMMON.CATEGORY}: +
    +
    + {categoryOptions?.map(category => { + return ( + +
    + + {category.name}({category.count}) +
    + + ) + })} +
    +
    + {/* 标签 */} + +
    + + {locale.COMMON.TAGS}: +
    +
    + {tagOptions?.map(tag => { + return ( +
    + +
    + ) + })} +
    +
    +
    + +} diff --git a/themes/heo/components/SideBar.js b/themes/heo/components/SideBar.js new file mode 100644 index 00000000..e43c4e12 --- /dev/null +++ b/themes/heo/components/SideBar.js @@ -0,0 +1,33 @@ +import BLOG from '@/blog.config' +import { useRouter } from 'next/router' +import MenuGroupCard from './MenuGroupCard' +import { MenuListSide } from './MenuListSide' + +/** + * 侧边抽屉 + * @param tags + * @param currentTag + * @returns {JSX.Element} + * @constructor + */ +const SideBar = (props) => { + const { siteInfo } = props + const router = useRouter() + return ( + + ) +} + +export default SideBar diff --git a/themes/heo/components/SideBarDrawer.js b/themes/heo/components/SideBarDrawer.js new file mode 100644 index 00000000..87125c05 --- /dev/null +++ b/themes/heo/components/SideBarDrawer.js @@ -0,0 +1,51 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +/** + * 侧边栏抽屉面板,可以从侧面拉出 + * @returns {JSX.Element} + * @constructor + */ +const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => { + const router = useRouter() + useEffect(() => { + const sideBarDrawerRouteListener = () => { + switchSideDrawerVisible(false) + } + router.events.on('routeChangeComplete', sideBarDrawerRouteListener) + return () => { + router.events.off('routeChangeComplete', sideBarDrawerRouteListener) + } + }, [router.events]) + + // 点击按钮更改侧边抽屉状态 + const switchSideDrawerVisible = (showStatus) => { + if (showStatus) { + onOpen && onOpen() + } else { + onClose && onClose() + } + const sideBarDrawer = window.document.getElementById('sidebar-drawer') + const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background') + + if (showStatus) { + sideBarDrawer?.classList.replace('-mr-72', 'mr-0') + sideBarDrawerBackground?.classList.replace('hidden', 'block') + } else { + sideBarDrawer?.classList.replace('mr-0', '-mr-72') + sideBarDrawerBackground?.classList.replace('block', 'hidden') + } + } + + return