Nobelium主题

This commit is contained in:
tangly1024
2022-12-24 18:53:49 +08:00
parent c1fc29749b
commit 678ca279ce
33 changed files with 1187 additions and 4 deletions

View File

@@ -1,2 +1,27 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=3.6.8
NEXT_PUBLIC_VERSION=3.6.8
NEXT_PUBLIC_WALINE_SERVER_URL=https://waline.tangly1024.com/
NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD=密码
NEXT_PUBLIC_NOTION_PROPERTY_TYPE=类型
NEXT_PUBLIC_NOTION_PROPERTY_TITLE=标题
NEXT_PUBLIC_NOTION_PROPERTY_STATUS=状态
NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY=摘要
NEXT_PUBLIC_NOTION_PROPERTY_SLUG=链接
NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY=分类
NEXT_PUBLIC_NOTION_PROPERTY_DATE=日期
NEXT_PUBLIC_NOTION_PROPERTY_TAGS=标签
NEXT_PUBLIC_NOTION_PROPERTY_ICON=图标
NOTION_PAGE_ID=29d5a78b858e4a3bbc13e51b5400fb82
#影院前线NOTION_PAGE_ID=c7c08fdeb087414584a7912b92081c75
NEXT_PUBLIC_THEME_SWITCH=true
NEXT_PUBLIC_WALINE_SERVER_URL=https://preview-waline.tangly1024.com
NEXT_PUBLIC_WALINE_RECENT=true
NEXT_PUBLIC_THEME=medium
#VERCEL_ENV=production
ENABLE_CACHE=true
NEXT_PUBLIC_COMMENT_ENV_ID=https://twikoo.tangly1024.com

42
components/Vercel.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -197,4 +197,12 @@ nav {
-webkit-box-orient: vertical;
word-wrap: break-word;
word-break: break-all;
}
}
.nobelium{
@apply flex flex-col justify-between
}
.nobelium .notion-code{
@apply max-w-2xl;
}

View File

@@ -5,7 +5,8 @@ import * as next from './next'
import * as fukasawa from './fukasawa'
import * as hexo from './hexo'
import * as medium from './medium'
import * as nobelium from './nobelium'
import * as example from './example'
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'example']
export { hexo, next, medium, fukasawa, example }
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'nobelium', 'example']
export { hexo, next, medium, fukasawa, nobelium, example }

View File

@@ -0,0 +1,7 @@
import LayoutBase from './LayoutBase'
export const Layout404 = (props) => {
return <LayoutBase {...props}>
404 Not found.
</LayoutBase>
}

View File

@@ -0,0 +1,62 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutArchive = props => {
const { posts } = props
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 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 (
<LayoutBase {...props}>
<div className="mb-10 pb-20 md:py-12 p-3 min-h-screen w-full">
{Object.keys(archivePosts).map(archiveTitle => (
<div key={archiveTitle}>
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
{archiveTitle}
</div>
<ul>
{archivePosts[archiveTitle].map(post => (
<li
key={post.id}
className="border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500"
>
<div id={post?.date?.start_date}>
<span className="text-gray-400">
{post.date?.start_date}
</span>{' '}
&nbsp;
<Link
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
>
<a className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
{post.title}
</a>
</Link>
</div>
</li>
))}
</ul>
</div>
))}
</div>
</LayoutBase>
)
}

View File

@@ -0,0 +1,41 @@
import CommonHead from '@/components/CommonHead'
import React from 'react'
import Header from './components/Header'
import { Footer } from './components/Footer'
import JumpToTopButton from './components/JumpToTopButton'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = props => {
const { children, meta, post } = props
const fullWidth = post?.fullWidth ?? false
return (
<div className='nobelium dark:text-gray-300 w-full overflow-hidden bg-white dark:bg-black min-h-screen'>
<CommonHead meta={meta} />
{/* 顶栏LOGO */}
<Header {...props} />
<main className={`m-auto flex-grow w-full transition-all ${
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
}`}>
{children}
</main>
<Footer {...props} />
<div className='fixed right-4 bottom-4'>
<JumpToTopButton />
</div>
</div>
)
}
export default LayoutBase

View File

@@ -0,0 +1,10 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import LayoutBase from './LayoutBase'
export const LayoutCategory = props => {
return <LayoutBase {...props}>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase >
}

View File

@@ -0,0 +1,19 @@
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutCategoryIndex = (props) => {
const { categories } = props
return <LayoutBase {...props}>
<div id='category-list' className='duration-200 flex flex-wrap'>
{categories && categories.map(category => {
return <Link key={category.name} href={`/category/${category.name}`} passHref>
<div
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
<i className='mr-4 fas fa-folder' />{category.name}({category.count})
</div>
</Link>
})}
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,13 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import LayoutBase from './LayoutBase'
export const LayoutIndex = props => {
return (
<LayoutBase {...props}>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase>
)
}

View File

@@ -0,0 +1,10 @@
import { BlogListPage } from './components/BlogListPage'
import LayoutBase from './LayoutBase'
export const LayoutPage = props => {
return (
<LayoutBase {...props}>
<BlogListPage {...props} />
</LayoutBase>
)
}

View File

@@ -0,0 +1,54 @@
import BLOG from '@/blog.config'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import SearchInput from './components/SearchInput'
import Mark from 'mark.js'
import LayoutBase from './LayoutBase'
import { isBrowser } from '@/lib/utils'
export const LayoutSearch = props => {
const { keyword } = props
const router = useRouter()
useEffect(() => {
setTimeout(() => {
const container = isBrowser() && document.getElementById('container')
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])
useEffect(() => {
setTimeout(() => {
if (keyword) {
const targets = document.getElementsByClassName('replace')
for (const container of targets) {
if (container && container.innerHTML) {
const re = new RegExp(`${keyword}`, 'gim')
container.innerHTML = container.innerHTML.replace(
re,
`<span class='text-red-500 border-b border-dashed'>${keyword}</span>`
)
}
}
}
}, 100)
}, [])
return <LayoutBase {...props}>
<div className='pb-12'>
<SearchInput {...props} />
</div>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
</LayoutBase>
}

View File

@@ -0,0 +1,31 @@
import LayoutBase from './LayoutBase'
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'
export const LayoutSlug = props => {
const { post, lock, validPassword } = props
if (!post) {
return <LayoutBase {...props} />
}
return (
<LayoutBase {...props}>
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && <div id="notion-article" className="px-2">
{post && <>
<ArticleInfo post={post} />
<NotionPage post={post} />
<Comment frontMatter={post}/>
<ArticleFooter />
</>}
</div>}
</LayoutBase>
)
}

View File

@@ -0,0 +1,57 @@
import React from 'react'
import BlogPost from './components/BlogPost'
import Tags from './components/Tags'
import LayoutBase from './LayoutBase'
export const LayoutTag = props => {
const { currentTag } = props
const [searchValue, setSearchValue] = React.useState('')
let filteredBlogPosts = []
const { posts } = props
if (posts) {
filteredBlogPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
})
}
return <LayoutBase {...props}>
<div className="relative">
<input
type="text"
placeholder={
currentTag ? `Search in #${currentTag}` : 'Search Articles'
}
className="block w-full border px-4 py-2 border-black bg-white text-black dark:bg-night dark:border-white dark:text-white"
onChange={e => setSearchValue(e.target.value)}
/>
<svg
className="absolute right-3 top-3 h-5 w-5 text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</div>
<Tags {...props}/>
<div className="article-container my-8">
{!filteredBlogPosts.length && (
<p className="text-gray-500 dark:text-gray-300">No posts found.</p>
)}
{filteredBlogPosts.slice(0, 20).map(post => (
<BlogPost key={post.id} post={post} />
))}
</div>
</LayoutBase >
}

View File

@@ -0,0 +1,21 @@
import Link from 'next/link'
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = (props) => {
const { tags } = props
return <LayoutBase {...props}>
<div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{tags.map(tag => {
return <div key={tag.name} className='p-2'>
<Link key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
<div className='font-light dark:text-gray-400'><i className='mr-1 fas fa-tag' /> {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a>
</Link>
</div>
})}
</div>
</div> </LayoutBase>
}

View File

@@ -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 <div className="flex justify-between font-medium text-gray-500 dark:text-gray-400">
<a>
<button
onClick={() => router.push(BLOG.path || '/')}
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
>
{locale.POST.BACK}
</button>
</a>
<a>
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="mt-2 cursor-pointer hover:text-black dark:hover:text-gray-100"
>
{locale.POST.TOP}
</button>
</a>
</div>
}

View File

@@ -0,0 +1,63 @@
import formatDate from '@/lib/formatDate'
import Image from 'next/image'
import BLOG from '@/blog.config'
import TagItem from './TagItem'
import { createHash } from 'crypto'
export const ArticleInfo = (props) => {
const { post } = props
const emailHash = createHash('md5')
.update(BLOG.CONTACT_EMAIL)
.digest('hex')
.trim()
.toLowerCase()
return <section className="flex-wrap flex mt-2 text-gray--600 dark:text-gray-400 font-light leading-8">
<div>
<div className="font-bold text-3xl text-black dark:text-white">
{post.title}
</div>
{post?.type !== 'Page' && <>
<nav className="flex mt-7 items-start text-gray-500 dark:text-gray-400">
<div className="flex mb-4">
<a href={BLOG.CONTACT_GITHUB || '#'} className="flex">
<Image
alt={BLOG.author}
width={24}
height={24}
src={`https://gravatar.com/avatar/${emailHash}`}
className="rounded-full"
/>
<p className="ml-2 md:block">{BLOG.author}</p>
</a>
<span className="block">&nbsp;/&nbsp;</span>
</div>
<div className="mr-2 mb-4 md:ml-0">
{formatDate(
post?.date?.start_date || post.createdTime,
BLOG.LANG
)}
</div>
{post.tags && (
<div className="flex flex-nowrap max-w-full overflow-x-auto article-tags">
{post.tags.map(tag => (
<TagItem key={tag} tag={tag} />
))}
</div>
)}
<span className="hidden busuanzi_container_page_pv mr-2">
<i className='mr-1 fas fa-eye' />
&nbsp;
<span className="mr-2 busuanzi_value_page_pv" />
</span>
</nav>
</>}
</div>
</section>
}

View File

@@ -0,0 +1,38 @@
import { useGlobal } from '@/lib/global'
/**
* 加密文章校验组件
* @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 = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
}
}
}
return <div id='container' className='w-full flex justify-center items-center h-96 font-sans'>
<div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex'>
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'></input>
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,44 @@
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.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
return <div className="w-full md:pr-12 mb-12">
<div id="container">
{posts?.map(post => (
<BlogPost key={post.id} post={post}/>
))}
</div>
<div className="flex justify-between text-xs">
<Link href={{ pathname: currentPage - 1 === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`, query: router.query.s ? { s: router.query.s } : {} }}>
<a className={`${showPrev ? ' ' : ' invisible block pointer-events-none '}no-underline py-2 px-3 rounded`}>
<button rel="prev" className="block cursor-pointer">
{locale.PAGINATION.PREV}
</button>
</a>
</Link>
<Link href={{ pathname: `${pagePrefix}/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}>
<a className={`${showNext ? ' ' : 'invisible pointer-events-none '} no-underline py-2 px-3 rounded`}>
<button rel="next" className="block cursor-pointer">
{locale.PAGINATION.NEXT}
</button>
</a>
</Link>
</div>
</div>
}

View File

@@ -0,0 +1,78 @@
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 <div id="container" className="w-full md:pr-12 mb-12" ref={targetRef}>
{postsToShow.map(p => (
<article key={p.id} className="mb-12" >
<h2 className="mb-4">
<Link href={`/${p.slug}`}>
<a className="text-black text-xl md:text-2xl no-underline hover:underline"> {p.title}</a>
</Link>
</h2>
<div className="mb-4 text-sm text-gray-700">
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
<span className="font-bold mx-1"> | </span>
<a href="#" className="text-gray-700">{p.category}</a>
<span className="font-bold mx-1"> | </span>
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
</div>
<p className="text-gray-700 leading-normal">
{p.summary}
</p>
</article>
))}
<div
onClick={handleGetMore}
className="w-full my-4 py-4 text-center cursor-pointer "
>
{' '}
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
</div>
</div>
}

View File

@@ -0,0 +1,29 @@
import Link from 'next/link'
import BLOG from '@/blog.config'
import formatDate from '@/lib/formatDate'
const BlogPost = ({ post }) => {
return (
<Link href={`${BLOG.SUB_PATH}/${post.slug}`}>
<a>
<article key={post.id} className="mb-6 md:mb-8">
<header className="flex flex-col justify-between md:flex-row md:items-baseline">
<h2 className="text-lg md:text-xl font-medium mb-2 cursor-pointer text-black dark:text-gray-100">
{post.title}
</h2>
<time className="flex-shrink-0 text-gray-600 dark:text-gray-400">
{formatDate(post?.date?.start_date || post.createdTime, BLOG.LANG)}
</time>
</header>
<main>
<p className="hidden md:block leading-8 text-gray-700 dark:text-gray-300">
{post.summary}
</p>
</main>
</article>
</a>
</Link>
)
}
export default BlogPost

View File

@@ -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 && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
<div className='dark:text-gray-400 text-gray-400 font-sans text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}><a >-- {comment.nick}</a></Link></div>
</div>)}
</>
}
export default ExampleRecentComments

View File

@@ -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 <footer
className={`mt-6 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 transition-all ${
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
}`}
>
<DarkModeButton className='text-center pt-4'/>
<hr className="border-gray-200 dark:border-gray-600" />
<div className="my-4 text-sm leading-6">
<div className="flex align-baseline justify-between flex-wrap">
<p>
© {BLOG.author} {copyrightDate}
</p>
<Vercel />
</div>
</div>
</footer>
}

View File

@@ -0,0 +1,128 @@
import { useEffect, useRef } from 'react'
import Link from 'next/link'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
const NavBar = (props) => {
const { customNav } = props
const { locale } = useGlobal()
let links = [
{ id: 2, name: locale.NAV.RSS, to: '/feed', show: true },
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: true },
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: true },
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: false },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: true }
]
if (customNav) {
links = links.concat(customNav)
}
return (
<div className="flex-shrink-0">
<ul className="flex flex-row">
{links.map(
link =>
link.show && (
<li
key={link.id}
className="block ml-4 text-black dark:text-gray-50 nav"
>
<Link href={link.to}>
<a>{link.name}</a>
</Link>
</li>
)
)}
</ul>
</div>
)
}
const Header = ({ navBarTitle, fullWidth }) => {
const useSticky = !BLOG.autoCollapsedNavBar
const navRef = useRef(null)
const sentinalRef = useRef([])
const handler = ([entry]) => {
if (navRef && navRef.current && useSticky) {
if (!entry.isIntersecting && entry !== undefined) {
navRef.current?.classList.add('sticky-nav-full')
} else {
navRef.current?.classList.remove('sticky-nav-full')
}
} else {
navRef.current?.classList.add('remove-sticky')
}
}
useEffect(() => {
const obvserver = new window.IntersectionObserver(handler)
obvserver.observe(sentinalRef.current)
// Don't touch this, I have no idea how it works XD
// return () => {
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
// }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sentinalRef])
return (
<>
<div className="observer-element h-4 md:h-12" ref={sentinalRef}></div>
<div
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 md:mb-12 py-8 bg-opacity-60 ${
!fullWidth ? 'max-w-3xl px-4' : 'px-4 md:px-24'
}`}
id="sticky-nav"
ref={navRef}
>
<div className="flex items-center">
<Link href="/">
<a aria-label={BLOG.title}>
<div className="h-6">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="24"
height="24"
className="fill-current text-black dark:text-white"
/>
<rect width="24" height="24" fill="url(#paint0_radial)" />
<defs>
<radialGradient
id="paint0_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(45) scale(39.598)"
>
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
</radialGradient>
</defs>
</svg>
</div>
</a>
</Link>
{navBarTitle
? (
<p className="ml-2 font-medium text-day dark:text-night header-name">
{navBarTitle}
</p>
)
: (
<p className="ml-2 font-medium text-day dark:text-night header-name">
{BLOG.title},{' '}
<span className="font-normal">{BLOG.description}</span>
</p>
)}
</div>
<NavBar />
</div>
</>
)
}
export default Header

View File

@@ -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 <div title={locale.POST.TOP} className='cursor-pointer p-2 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
><i className='fas fa-angle-up text-2xl' />
</div>
}
export default JumpToTopButton

View File

@@ -0,0 +1,39 @@
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
/**
* 菜单导航
* @param {*} props
* @returns
*/
export const Nav = (props) => {
const { customNav } = props
const { locale } = useGlobal()
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 <nav className="w-full bg-white md:pt-0 px-6 relative z-20 border-t border-b border-gray-light dark:border-hexo-black-gray dark:bg-black">
<div className="container mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start">
<div className="w-full md:w-2/3 text-center md:text-left py-4 flex flex-wrap justify-center items-stretch md:justify-start md:items-start">
{links.map(link => {
return link && <Link href={link.to} key={link.to}>
<a className="px-2 md:pl-0 md:mr-3 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light">
{link.name}
</a>
</Link>
})}
</div>
<div className="w-full md:w-1/3 text-center md:text-right">
{/* <!-- extra links --> */}
</div>
</div>
</nav>
}

View File

@@ -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 <section className='flex w-full bg-gray-100'>
<input
ref={searchInputRef}
type='text'
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
className={'w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput}
onCompositionUpdate={lockSearchInput}
onCompositionEnd={unLockSearchInput}
onChange={e => updateSearchKey(e.target.value)}
defaultValue={currentSearch || ''}
/>
<div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
onClick={handleSearch}>
<i className={'hover:text-black transform duration-200 text-gray-500 cursor-pointer fas fa-search'} />
</div>
{(showClean &&
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
<i className='hover:text-black transform duration-200 text-gray-400 cursor-pointer fas fa-times' onClick={cleanSearch} />
</div>
)}
</section>
}
export default SearchInput

View File

@@ -0,0 +1,55 @@
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 <div className="w-full md:w-64 sticky top-8">
<aside className="rounded shadow overflow-hidden mb-6">
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.CATEGORY}</h3>
<div className="p-4">
<ul className="list-reset leading-normal">
{categories?.map(category => {
return <Link key={category.name} href={`/category/${category.name}`} passHref>
<li> <a href="#" className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
</Link>
})}
</ul>
</div>
</aside>
<aside className="rounded shadow overflow-hidden mb-6">
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.LATEST_POSTS}</h3>
<div className="p-4">
<ul className="list-reset leading-normal">
{latestPosts?.map(p => {
return <Link key={p.id} href={`/${p.slug}`} passHref>
<li> <a href="#" className="text-gray-darkest text-sm">{p.title}</a></li>
</Link>
})}
</ul>
</div>
</aside>
{BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT && <aside className="rounded shadow overflow-hidden mb-6">
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.RECENT_COMMENTS}</h3>
<div className="p-4">
<ExampleRecentComments/>
</div>
</aside>}
<aside className="rounded overflow-hidden mb-6">
<Live2D />
</aside>
</div>
}

View File

@@ -0,0 +1,13 @@
import Link from 'next/link'
const TagItem = ({ tag }) => (
<Link href={`/tag/${encodeURIComponent(tag)}`}>
<a>
<p className="mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600">
{tag}
</p>
</a>
</Link>
)
export default TagItem

View File

@@ -0,0 +1,38 @@
import Link from 'next/link'
const Tags = (props) => {
const { tags, tag } = props
const currentTag = tag
if (!tags) return null
return (
<div className="tag-container">
<ul className="flex max-w-full mt-4 overflow-x-auto">
{Object.keys(tags).map(key => {
const tag = tags[key]
const selected = tag.name === currentTag
return (
<li
key={tag.id}
className={`mr-3 font-medium border whitespace-nowrap dark:text-gray-300 ${
selected
? 'text-white bg-black border-black dark:bg-gray-600 dark:border-gray-600'
: 'bg-gray-100 border-gray-100 text-gray-400 dark:bg-night dark:border-gray-800'
}`}
>
<Link
key={tag.id}
href={selected ? '/search' : `/tag/${encodeURIComponent(tag.name)}`}
>
<a className="px-4 py-2 block">
{`${tag.name} (${tag.count})`}
</a>
</Link>
</li>
)
})}
</ul>
</div>
)
}
export default Tags

View File

@@ -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 <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
<p className="leading-loose text-gray-dark">
{description}
</p>
</div>
}

View File

@@ -0,0 +1,4 @@
const CONFIG_EMPTY = {
TEST_CONFIG: 'TESET'
}
export default CONFIG_EMPTY

25
themes/nobelium/index.js Normal file
View File

@@ -0,0 +1,25 @@
import CONFIG_EMPTY from './config_nobelium'
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
}