diff --git a/themes/example/Layout404.js b/themes/example/Layout404.js new file mode 100644 index 00000000..cd28a607 --- /dev/null +++ b/themes/example/Layout404.js @@ -0,0 +1,6 @@ + +export const Layout404 = () => { + return
+ 404 Not found. +
+} diff --git a/themes/example/LayoutArchive.js b/themes/example/LayoutArchive.js new file mode 100644 index 00000000..04a6d647 --- /dev/null +++ b/themes/example/LayoutArchive.js @@ -0,0 +1,69 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import LayoutBase from './LayoutBase' + +export const LayoutArchive = props => { + const { posts } = props + const { locale } = useGlobal() + const postsSortByDate = Object.create(posts) + + postsSortByDate.sort((a, b) => { + const dateA = new Date(a?.date.start_date || a.createdTime) + const dateB = new Date(b?.date.start_date || b.createdTime) + return dateB - dateA + }) + + const meta = { + title: `${locale.NAV.ARCHIVE} | ${BLOG.TITLE}`, + description: BLOG.DESCRIPTION, + type: 'website' + } + + const archivePosts = {} + + postsSortByDate.forEach(post => { + const date = post.date.start_date.slice(0, 7) + if (archivePosts[date]) { + archivePosts[date].push(post) + } else { + archivePosts[date] = [post] + } + }) + return ( + +
+ {Object.keys(archivePosts).map(archiveTitle => ( +
+
+ {archiveTitle} +
+
    + {archivePosts[archiveTitle].map(post => ( +
  • +
    + + {post.date.start_date} + {' '} +   + + + {post.title} + + +
    +
  • + ))} +
+
+ ))} +
+
+ ) +} diff --git a/themes/example/LayoutBase.js b/themes/example/LayoutBase.js new file mode 100644 index 00000000..7898c47d --- /dev/null +++ b/themes/example/LayoutBase.js @@ -0,0 +1,135 @@ +import CommonHead from '@/components/CommonHead' +import Live2D from '@/components/Live2D' +import Link from 'next/link' +import React from 'react' +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +/** + * 基础布局 采用左右两侧布局,移动端使用顶部导航栏 + + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, meta, customNav } = props + const { locale } = useGlobal() + const d = new Date() + const currentYear = d.getFullYear() + const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-' + + let links = [ + { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' }, + { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' }, + { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' }, + { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' } + ] + + if (customNav) { + links = links.concat(customNav) + } + + return ( +
+ + {/* 导航菜单 */} +
+
+
+ + + +
{BLOG.TITLE}
+
+ +
+ +
+
+ + {/* 内容主体 */} +
+
{children}
+
+
+ +
+
+
+ + {/* 页脚 */} + +
+ ) +} + +export default LayoutBase diff --git a/themes/example/LayoutCategory.js b/themes/example/LayoutCategory.js new file mode 100644 index 00000000..85d399f6 --- /dev/null +++ b/themes/example/LayoutCategory.js @@ -0,0 +1,48 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useState } from 'react' +import LayoutBase from './LayoutBase' + +export const LayoutCategory = props => { + const { category, posts } = props + const { locale } = useGlobal() + + const [page, updatePage] = 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) + } + + return ( + + Category - {category} + {postsToShow.map(p => ( +
+ + {p.title} + +
{p.summary}
+
+ ))} +
+
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+
+ ) +} diff --git a/themes/example/LayoutCategoryIndex.js b/themes/example/LayoutCategoryIndex.js new file mode 100644 index 00000000..b748b140 --- /dev/null +++ b/themes/example/LayoutCategoryIndex.js @@ -0,0 +1,25 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import LayoutBase from './LayoutBase' + +export const LayoutCategoryIndex = (props) => { + const { categories } = props + const { locale } = useGlobal() + return +
+
+ {locale.COMMON.CATEGORY}: +
+
+ {categories && categories.map(category => { + return +
+ {category.name}({category.count}) +
+ + })} +
+
+
+} diff --git a/themes/example/LayoutIndex.js b/themes/example/LayoutIndex.js new file mode 100644 index 00000000..817bcdfb --- /dev/null +++ b/themes/example/LayoutIndex.js @@ -0,0 +1,52 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import LayoutBase from './LayoutBase' + +export const LayoutIndex = props => { + const { posts, postCount } = props + + const { locale } = useGlobal() + const router = useRouter() + const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) + + const page = 1 + const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount + + const currentPage = +page + return ( + + {posts.map(p => ( +
+ + {p.title} + +
{p.summary}
+
+ ))} + + +
+ ) +} diff --git a/themes/example/LayoutPage.js b/themes/example/LayoutPage.js new file mode 100644 index 00000000..2f5f795a --- /dev/null +++ b/themes/example/LayoutPage.js @@ -0,0 +1,52 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import LayoutBase from './LayoutBase' + +export const LayoutPage = (props) => { + const { page } = props + const { posts, postCount } = props + + const { locale } = useGlobal() + const router = useRouter() + const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) + + const showNext = page < totalPage && posts.length === BLOG.POSTS_PER_PAGE && posts.length < postCount + + const currentPage = +page + return ( + + {posts.map(p => ( +
+ + {p.title} + +
{p.summary}
+
+ ))} + + +
+ ) +} diff --git a/themes/example/LayoutSearch.js b/themes/example/LayoutSearch.js new file mode 100644 index 00000000..d24024a5 --- /dev/null +++ b/themes/example/LayoutSearch.js @@ -0,0 +1,60 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useEffect, useState } from 'react' +import SearchInput from './components/SearchInput' +import LayoutBase from './LayoutBase' + +export const LayoutSearch = props => { + const { keyword, posts } = props + useEffect(() => { + setTimeout(() => { + const container = document.getElementById('container') + if (container && container.innerHTML) { + const re = new RegExp(`${keyword}`, 'gim') + container.innerHTML = container.innerHTML.replace(re, `${keyword}`) + } + }, 100) + }) + + const { locale } = useGlobal() + + const [page, updatePage] = 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) + } + + return ( + +

Search - {keyword}

+ + {postsToShow.map(p => ( +
+ + {p.title} + +
{p.summary}
+
+ ))} +
+
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+
+ ) +} diff --git a/themes/example/LayoutSlug.js b/themes/example/LayoutSlug.js new file mode 100644 index 00000000..7ff9b624 --- /dev/null +++ b/themes/example/LayoutSlug.js @@ -0,0 +1,100 @@ +import BLOG from '@/blog.config' +import { getPageTableOfContents } from 'notion-utils' +import 'prismjs' +import 'prismjs/components/prism-bash' +import 'prismjs/components/prism-javascript' +import 'prismjs/components/prism-markup' +import 'prismjs/components/prism-python' +import 'prismjs/components/prism-typescript' +import { + Code, + Collection, + CollectionRow, + Equation, + NotionRenderer +} from 'react-notion-x' +import LayoutBase from './LayoutBase' +import { useRef, useEffect } from 'react' +import { ArticleLock } from './components/ArticleLock' +import mediumZoom from 'medium-zoom' + +const mapPageUrl = id => { + return 'https://www.notion.so/' + id.replace(/-/g, '') +} + +export const LayoutSlug = props => { + const { post, lock, validPassword } = props + const meta = { + title: `${post.title} | ${BLOG.TITLE}`, + description: post.summary, + type: 'article', + tags: post.tags + } + + if (!lock && post?.blockMap?.block) { + post.content = Object.keys(post.blockMap.block) + post.toc = getPageTableOfContents(post, post.blockMap) + } + + const zoom = + typeof window !== 'undefined' && + mediumZoom({ + container: '.notion-viewport', + background: 'rgba(0, 0, 0, 0.2)', + margin: getMediumZoomMargin() + }) + const zoomRef = useRef(zoom ? zoom.clone() : null) + useEffect(() => { + // 将所有container下的所有图片添加medium-zoom + const container = document.getElementById('notion-article') + const imgList = container?.getElementsByTagName('img') + if (imgList && zoomRef.current) { + for (let i = 0; i < imgList.length; i++) { + zoomRef.current.attach(imgList[i]) + } + } + }) + return ( + +
+

{post?.title}

+ + {lock && } + + {!lock &&
+ {post.blockMap && ( + + )} +
} + +
+
+ ) +} + +function getMediumZoomMargin () { + const width = window.innerWidth + + if (width < 500) { + return 8 + } else if (width < 800) { + return 20 + } else if (width < 1280) { + return 30 + } else if (width < 1600) { + return 40 + } else if (width < 1920) { + return 48 + } else { + return 72 + } +} diff --git a/themes/example/LayoutTag.js b/themes/example/LayoutTag.js new file mode 100644 index 00000000..e1a549f4 --- /dev/null +++ b/themes/example/LayoutTag.js @@ -0,0 +1,49 @@ +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useState } from 'react' +import LayoutBase from './LayoutBase' + +export const LayoutTag = props => { + const { tag, posts } = props + const { locale } = useGlobal() + + const [page, updatePage] = 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) + } + + return ( + + Tag - {tag} + {postsToShow.map(p => ( +
+ + {p.title} + +
{p.summary}
+
+ ))} +
+
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+
+ ) +} diff --git a/themes/example/LayoutTagIndex.js b/themes/example/LayoutTagIndex.js new file mode 100644 index 00000000..3fb41c9f --- /dev/null +++ b/themes/example/LayoutTagIndex.js @@ -0,0 +1,24 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import LayoutBase from './LayoutBase' + +export const LayoutTagIndex = (props) => { + const { tags } = props + const { locale } = useGlobal() + return +
+
{locale.COMMON.TAGS}:
+
+ { tags.map(tag => { + return + }) } +
+
+} diff --git a/themes/example/components/ArticleLock.js b/themes/example/components/ArticleLock.js new file mode 100644 index 00000000..bfdd00ce --- /dev/null +++ b/themes/example/components/ArticleLock.js @@ -0,0 +1,40 @@ +import { useGlobal } from '@/lib/global' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleLock = props => { + const { password, validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (p && p.value && p.value === password) { + validPassword(true) + } else { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
${locale.COMMON.PASSWORD_ERROR}
` + } + } + } + + return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+ +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+
+} diff --git a/themes/example/components/SearchInput.js b/themes/example/components/SearchInput.js new file mode 100644 index 00000000..6a95ba01 --- /dev/null +++ b/themes/example/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/example/config_empty.js b/themes/example/config_empty.js new file mode 100644 index 00000000..9f1ac567 --- /dev/null +++ b/themes/example/config_empty.js @@ -0,0 +1,4 @@ +const CONFIG_EMPTY = { + TEST_CONFIG: 'TESET' +} +export default CONFIG_EMPTY diff --git a/themes/example/index.js b/themes/example/index.js new file mode 100644 index 00000000..95b0a6b2 --- /dev/null +++ b/themes/example/index.js @@ -0,0 +1,25 @@ +import CONFIG_EMPTY from './config_empty' +import { LayoutIndex } from './LayoutIndex' +import { LayoutSearch } from './LayoutSearch' +import { LayoutArchive } from './LayoutArchive' +import { LayoutSlug } from './LayoutSlug' +import { Layout404 } from './Layout404' +import { LayoutCategory } from './LayoutCategory' +import { LayoutCategoryIndex } from './LayoutCategoryIndex' +import { LayoutPage } from './LayoutPage' +import { LayoutTag } from './LayoutTag' +import { LayoutTagIndex } from './LayoutTagIndex' + +export { + CONFIG_EMPTY as THEME_CONFIG, + LayoutIndex, + LayoutSearch, + LayoutArchive, + LayoutSlug, + Layout404, + LayoutCategory, + LayoutCategoryIndex, + LayoutPage, + LayoutTag, + LayoutTagIndex +}