Merge remote-tracking branch 'origin/main' into theme-medium

# Conflicts:
#	themes/index.js
This commit is contained in:
tangly1024
2022-03-03 16:34:50 +08:00
40 changed files with 211 additions and 142 deletions

View File

@@ -1,8 +1,9 @@
import lang from './lang' import lang from './lang'
import { useContext, createContext, useState, useEffect } from 'react' import { useContext, createContext, useState } from 'react'
import Router from 'next/router' import Router from 'next/router'
import { initTheme, loadUserThemeFromCookies } from './theme' import { initDarkMode } from './theme'
const GlobalContext = createContext() const GlobalContext = createContext()
let hasInit = false
/** /**
* 全局变量Provider包括语言本地化、样式主题、搜索词 * 全局变量Provider包括语言本地化、样式主题、搜索词
@@ -11,8 +12,8 @@ const GlobalContext = createContext()
* @constructor * @constructor
*/ */
export function GlobalContextProvider ({ children }) { export function GlobalContextProvider ({ children }) {
const [locale, changeLocale] = useState(generateLocaleDict('en-US')) const [locale, updateLocale] = useState(generateLocaleDict('en-US'))
const [theme, changeTheme] = useState(loadUserThemeFromCookies()) const [isDarkMode, updateDarkMode] = useState(false)
const [onLoading, changeLoadingState] = useState(false) const [onLoading, changeLoadingState] = useState(false)
Router.events.on('routeChangeStart', (...args) => { Router.events.on('routeChangeStart', (...args) => {
changeLoadingState(true) changeLoadingState(true)
@@ -23,13 +24,17 @@ export function GlobalContextProvider ({ children }) {
}) })
// 服务端静态渲染在渲染hooks后根据前端变量做初始化工作 // 服务端静态渲染在渲染hooks后根据前端变量做初始化工作
useEffect(() => { setTimeout(() => {
initTheme(theme, changeTheme) console.log('初始化', hasInit, isDarkMode)
initLocale(locale, changeLocale) if (!hasInit) {
}) hasInit = true
initLocale(locale, updateLocale)
initDarkMode(isDarkMode, updateDarkMode)
}
}, 1000)
return ( return (
<GlobalContext.Provider value={{ onLoading, locale, theme, changeTheme }}> <GlobalContext.Provider value={{ onLoading, locale, isDarkMode, updateDarkMode }}>
{children} {children}
</GlobalContext.Provider> </GlobalContext.Provider>
) )

View File

@@ -49,7 +49,6 @@ export async function getAllPosts ({ notionPageData, from, pageType }) {
}) })
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
console.warn('文章列表为空')
const cacheKey = 'page_block_' + BLOG.NOTION_PAGE_ID const cacheKey = 'page_block_' + BLOG.NOTION_PAGE_ID
await delCacheData(cacheKey) await delCacheData(cacheKey)
} }

View File

@@ -1,37 +1,45 @@
import cookie from 'react-cookies' import cookie from 'react-cookies'
import BLOG from '@/blog.config'
/** /**
* 初始化主题 * 初始化主题
* @param theme 用户默认主题state * @param isDarkMode
* @param changeTheme 更改主题ChangeState函数 * @param updateDarkMode 更改主题ChangeState函数
* @description 读取cookie中存的用户主题 * @description 读取cookie中存的用户主题
*/ */
export const initTheme = (theme, changeTheme) => { export const initDarkMode = (isDarkMode, updateDarkMode) => {
// 若未指定主题,则从时间和浏览器偏好中决定初始主题 if (!isDarkMode) {
if (!theme) { isDarkMode = isPreferDark()
const date = new Date()
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
const useDark = prefersDarkMode || (date.getHours() >= 18 || date.getHours() < 6)
if (useDark) {
theme = 'dark'
} else {
theme = 'light'
}
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const htmlElement = document.getElementsByTagName('html') updateDarkMode(isDarkMode)
htmlElement.className = '' saveDarkModeToCookies(isDarkMode)
changeTheme(theme) document.getElementsByTagName('html')[0].setAttribute('class', isDarkMode ? 'dark' : 'light')
saveTheme(theme)
htmlElement.classList?.add(theme)
} }
} }
/**
* 是否优先深色模式
* @returns {*}
*/
export function isPreferDark () {
if (BLOG.APPEARANCE === 'dark') {
return true
}
if (BLOG.APPEARANCE === 'auto') {
// 系统深色模式或时间是夜间时,强行置为夜间模式
const date = new Date()
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDarkMode || (date.getHours() >= 18 || date.getHours() < 6)
}
return false
}
/** /**
* 读取默认主题 * 读取默认主题
* @returns {*} * @returns {*}
*/ */
export const loadUserThemeFromCookies = () => { export const loadDarkModeFromCookies = () => {
return cookie.load('theme') return cookie.load('theme')
} }
@@ -39,6 +47,6 @@ export const loadUserThemeFromCookies = () => {
* 保存默认主题 * 保存默认主题
* @param newTheme * @param newTheme
*/ */
export const saveTheme = (newTheme) => { export const saveDarkModeToCookies = (newTheme) => {
cookie.save('theme', newTheme, { path: '/' }) cookie.save('theme', newTheme, { path: '/' })
} }

View File

@@ -11,7 +11,7 @@ class MyDocument extends Document {
render () { render () {
return ( return (
<Html lang={BLOG.LANG}> <Html lang={BLOG.LANG} className='test'>
<Head> <Head>
<link rel='icon' href='/favicon.ico' /> <link rel='icon' href='/favicon.ico' />
<link rel='icon' href='/favicon.svg' type='image/svg+xml' /> <link rel='icon' href='/favicon.svg' type='image/svg+xml' />

View File

@@ -4,6 +4,11 @@ import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { getDataFromCache } from '@/lib/cache/cache_manager' import { getDataFromCache } from '@/lib/cache/cache_manager'
/**
* 服务端搜索
* @param {*} param0
* @returns
*/
export async function getServerSideProps ({ params: { keyword } }) { export async function getServerSideProps ({ params: { keyword } }) {
const { const {
allPosts, allPosts,
@@ -110,18 +115,16 @@ async function filterByMemCache (allPosts, keyword) {
// console.log('搜索是否命中缓存', page !== null, indexContent) // console.log('搜索是否命中缓存', page !== null, indexContent)
post.results = [] post.results = []
let hitCount = 0 let hitCount = 0
const re = new RegExp(`${keyword}`, 'gim')
for (const i in indexContent) { for (const i in indexContent) {
const c = indexContent[i] const c = indexContent[i]
const index = c.toLowerCase().indexOf(keyword.toLowerCase()) const index = c.toLowerCase().indexOf(keyword.toLowerCase())
const referText = c?.replace(re, `<span class='text-red-500'>${keyword}</span>`)
if (index > -1) { if (index > -1) {
hit = true hit = true
hitCount += 1 hitCount += 1
post.results.push(`<span>${referText}</span>`) post.results.push(c)
} else { } else {
if ((post.results.length - 1) / hitCount < 3 || i === 0) { if ((post.results.length - 1) / hitCount < 3 || i === 0) {
post.results.push(`<span>${referText}</span>`) post.results.push(c)
} }
} }
} }

View File

@@ -4,6 +4,9 @@ import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
/**
* 浏览器前端搜索
*/
export async function getStaticProps () { export async function getStaticProps () {
const { const {
allPosts, allPosts,

View File

@@ -1,8 +1,23 @@
import LayoutBase from './LayoutBase' import LayoutBase from './LayoutBase'
import BlogListPage from './components/BlogListPage' import BlogListPage from './components/BlogListPage'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export const LayoutSearch = (props) => { export const LayoutSearch = (props) => {
return <LayoutBase {...props}> const { keyword } = props
const router = useRouter()
const currentSearch = keyword || router?.query?.s
useEffect(() => {
setTimeout(() => {
const container = document.getElementById('container')
if (container && container.innerHTML) {
const re = new RegExp(`${currentSearch}`, 'gim')
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${currentSearch}</span>`)
}
},
100)
})
return <LayoutBase {...props} currentSearch={currentSearch}>
<BlogListPage {...props}/> <BlogListPage {...props}/>
</LayoutBase> </LayoutBase>
} }

View File

@@ -9,7 +9,7 @@ import Catalog from './Catalog'
function AsideLeft (props) { function AsideLeft (props) {
const { tags, currentTag, categories, currentCategory, post } = props const { tags, currentTag, categories, currentCategory, post } = props
return <div className='w-72 bg-white min-h-screen px-10 py-14 hidden lg:block'> return <div className='w-72 bg-white dark:bg-gray-800 min-h-screen px-10 py-14 hidden lg:block'>
<Logo /> <Logo />
<section className='flex flex-col text-gray-600'> <section className='flex flex-col text-gray-600'>
@@ -19,11 +19,11 @@ function AsideLeft (props) {
<section className='flex flex-col text-gray-600'> <section className='flex flex-col text-gray-600'>
<hr className='w-12 my-8' /> <hr className='w-12 my-8' />
<SearchInput/> <SearchInput {...props}/>
</section> </section>
<section className='flex flex-col'> <section className='flex flex-col dark:text-gray-300'>
<hr className='w-12 my-8 ' /> <hr className='w-12 my-8' />
{ BLOG.DESCRIPTION } { BLOG.DESCRIPTION }
</section> </section>

View File

@@ -51,7 +51,7 @@ const Catalog = ({ toc }) => {
}, throttleMs)) }, throttleMs))
return <div> return <div>
<div className='w-full'><i className='mr-1 fas fa-stream' /> 目录</div> <div className='w-full dark:text-gray-300'><i className='mr-1 fas fa-stream' /> 目录</div>
<nav className='font-sans overflow-y-auto scroll-hidden text-black'> <nav className='font-sans overflow-y-auto scroll-hidden text-black'>
{toc.map((tocItem) => { {toc.map((tocItem) => {
const id = uuidToId(tocItem.id) const id = uuidToId(tocItem.id)
@@ -59,7 +59,7 @@ const Catalog = ({ toc }) => {
<a <a
key={id} key={id}
href={`#${id}`} href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-300
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `} notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
> >
<span <span

View File

@@ -24,10 +24,10 @@ function GroupMenu ({ customNav }) {
if (link.show) { if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={`${link.to}`} title={link.to} href={link.to} > return <Link key={`${link.to}`} title={link.to} href={link.to} >
<a className={'py-0.5 duration-500 justify-between text-gray-500 hover:text-black cursor-pointer flex flex-nowrap items-center ' + <a className={'py-0.5 duration-500 justify-between text-gray-500 dark:text-gray-300 hover:text-black hover:underline cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'text-black' : ' ')} > (selected ? 'text-black' : ' ')} >
<div className='my-auto items-center justify-center flex '> <div className='my-auto items-center justify-center flex '>
<div className={ 'text-gray-500 hover:text-black'}>{link.name}</div> <div className={ 'hover:text-black'}>{link.name}</div>
</div> </div>
{link.slot} {link.slot}
</a> </a>

View File

@@ -4,7 +4,7 @@ import Link from 'next/link'
function Logo () { function Logo () {
return <section className='flex'> return <section className='flex'>
<Link href='/'> <Link href='/'>
<a className='hover:bg-black hover:text-white border-black border-2 duration-500 px-4 py-2 cursor-pointer font-black'>{BLOG.TITLE}</a> <a className='hover:bg-black hover:text-white border-black border-2 duration-500 px-4 py-2 cursor-pointer dark:text-gray-300 dark:border-gray-300 font-black'>{BLOG.TITLE}</a>
</Link> </Link>
</section> </section>
} }

View File

@@ -62,9 +62,9 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
defaultValue={searchKey} defaultValue={searchKey}
/> />
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2' <div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}> onClick={() => { handleSearch(searchKey) }}>
<i className={`hover:text-black transform duration-200 text-gray-500 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'}`} /> <i className={`hover:text-black transform duration-200 text-gray-500 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'}`} />
</div> </div>
{(searchKey && searchKey.length && {(searchKey && searchKey.length &&

View File

@@ -38,7 +38,7 @@ const LayoutBase = (props) => {
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [show]) }, [show])
return (<div className='bg-white'> return (<div className='bg-white dark:bg-gray-900'>
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<TopNav {...props}/> <TopNav {...props}/>

View File

@@ -14,7 +14,7 @@ export const LayoutCategoryIndex = props => {
} }
return ( return (
<LayoutBase {...props} meta={meta}> <LayoutBase {...props} meta={meta}>
<Card className="bg-white dark:bg-gray-700 w-full min-h-screen"> <Card className="w-full min-h-screen">
<div className="dark:text-gray-200 mb-5 mx-3"> <div className="dark:text-gray-200 mb-5 mx-3">
<i className="mr-4 fas fa-th" /> <i className="mr-4 fas fa-th" />
{locale.COMMON.CATEGORY}: {locale.COMMON.CATEGORY}:

View File

@@ -1,9 +1,26 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import BlogPostListPage from './components/BlogPostListPage' import BlogPostListPage from './components/BlogPostListPage'
import LayoutBase from './LayoutBase' import LayoutBase from './LayoutBase'
export const LayoutSearch = (props) => { export const LayoutSearch = (props) => {
return <LayoutBase {...props}> const { keyword } = props
<BlogPostListPage {...props}/> const router = useRouter()
const currentSearch = keyword || router?.query?.s
useEffect(() => {
setTimeout(() => {
const container = document.getElementById('container')
if (container && container.innerHTML) {
const re = new RegExp(`${currentSearch}`, 'gim')
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${currentSearch}</span>`)
}
},
100)
})
return <LayoutBase {...props} currentSearch={currentSearch}>
<div ic='container'>
<BlogPostListPage {...props}/>
</div>
</LayoutBase> </LayoutBase>
} }

View File

@@ -53,7 +53,7 @@ export const LayoutSlug = props => {
showTag={false} showTag={false}
floatSlot={floatSlot} floatSlot={floatSlot}
> >
<div className="w-full lg:shadow-md lg:hover:shadow-2xl lg:border lg:border-gray-100 lg:rounded-xl lg:px-2 lg:py-4 lg:bg-white lg:dark:bg-gray-800 lg:duration-300"> <div className="w-full dark:border-gray-600 lg:shadow-md lg:hover:shadow-2xl lg:border lg:border-gray-100 lg:rounded-xl lg:px-2 lg:py-4 lg:bg-white lg:dark:bg-gray-800 lg:duration-300">
<ArticleDetail {...props} /> <ArticleDetail {...props} />
</div> </div>

View File

@@ -34,9 +34,7 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
}) })
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto md:w-full md:px-5 "> return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">
<article itemScope itemType="https://schema.org/Movie" <article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased bg-white dark:bg-gray-800" >
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
>
{/* Notion文章主体 */} {/* Notion文章主体 */}
<section id='notion-article' className='px-5'> <section id='notion-article' className='px-5'>

View File

@@ -9,7 +9,7 @@ import CONFIG_HEXO from '../config_hexo'
const BlogPostCard = ({ post, showSummary }) => { const BlogPostCard = ({ post, showSummary }) => {
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
return ( return (
<div className='w-full shadow hover:shadow-2xl border border-gray-100 rounded-xl bg-white dark:bg-gray-800 duration-300'> <div className='w-full shadow hover:shadow-2xl border border-gray-100 dark:border-gray-600 rounded-xl bg-white dark:bg-gray-800 duration-300'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse lg:flex-row justify-between duration-300'> <div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse lg:flex-row justify-between duration-300'>
<div className='lg:p-8 p-4 flex flex-col w-full'> <div className='lg:p-8 p-4 flex flex-col w-full'>

View File

@@ -1,7 +1,7 @@
const Card = ({ children, headerSlot, className }) => { const Card = ({ children, headerSlot, className }) => {
return <div className={className}> return <div className={className}>
<>{headerSlot}</> <>{headerSlot}</>
<section className="shadow-md hover:shadow-2xl border border-gray-100 rounded-xl px-2 py-4 bg-white dark:bg-gray-800 duration-300"> <section className="shadow-md hover:shadow-2xl dark:text-gray-300 border border-gray-100 dark:border-gray-700 rounded-xl px-2 py-4 bg-white dark:bg-gray-800 duration-300">
{children} {children}
</section> </section>
</div> </div>

View File

@@ -63,7 +63,7 @@ const Catalog = ({ toc }) => {
<a <a
key={id} key={id}
href={`#${id}`} href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-200
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `} notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
> >
<span <span

View File

@@ -14,7 +14,7 @@ let autoScroll = false
*/ */
export default function Header () { export default function Header () {
const [typed, changeType] = useState() const [typed, changeType] = useState()
const { theme } = useGlobal() const { isDarkMode } = useGlobal()
useEffect(() => { useEffect(() => {
scrollTrigger() scrollTrigger()
@@ -76,7 +76,7 @@ export default function Header () {
} }
const updateTopNav = () => { const updateTopNav = () => {
if (theme !== 'dark') { if (!isDarkMode) {
const stickyNavElement = document.getElementById('sticky-nav') const stickyNavElement = document.getElementById('sticky-nav')
if (window.scrollY < window.innerHeight) { if (window.scrollY < window.innerHeight) {
stickyNavElement?.classList?.add('dark') stickyNavElement?.classList?.add('dark')

View File

@@ -47,7 +47,7 @@ export default function HeaderArticle ({ post }) {
</div> </div>
<section className="flex-wrap shadow-text flex text-sm justify-center mt-2 text-white dark:text-gray-400 font-light leading-8"> <section className="flex-wrap shadow-text flex text-sm justify-center mt-2 text-white dark:text-gray-400 font-light leading-8">
<div> <div className='dark:text-gray-200'>
{post.category && <> {post.category && <>
<Link href={`/category/${post.category}`} passHref> <Link href={`/category/${post.category}`} passHref>
<a className="cursor-pointer mr-2 dark:hover:text-white border-b dark:border-gray-500 border-dashed"> <a className="cursor-pointer mr-2 dark:hover:text-white border-b dark:border-gray-500 border-dashed">

View File

@@ -8,7 +8,7 @@ const MenuButtonGroup = (props) => {
const { postCount, customNav } = props const { postCount, customNav } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const router = useRouter() const router = useRouter()
const archiveSlot = <div className='bg-blue-300 dark:bg-blue-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div> const archiveSlot = <div className='bg-blue-300 dark:bg-blue-400 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
let links = [ let links = [
{ icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true }, { icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true },

View File

@@ -52,7 +52,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
<input <input
ref={searchInputRef} ref={searchInputRef}
type='text' type='text'
className={'w-full rounded-lg text-sm pl-5 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100'} className={'w-full rounded-lg text-sm pl-5 transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}
onCompositionUpdate={lockSearchInput} onCompositionUpdate={lockSearchInput}
@@ -61,14 +61,14 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
defaultValue={currentSearch} defaultValue={currentSearch}
/> />
<div className='-ml-8 cursor-pointer dark:hover:bg-gray-800 float-right items-center justify-center py-2' <div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}> onClick={() => { handleSearch(searchKey) }}>
<i className={`hover:text-black transform duration-200 text-gray-500 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'}`} /> <i className={`hover:text-black transform duration-200 text-gray-500 dark:text-gray-200 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'}`} />
</div> </div>
{(searchKey && searchKey.length && {(searchKey && searchKey.length &&
<div className='-ml-12 cursor-pointer dark:hover:bg-gray-800 float-right items-center justify-center py-2'> <div className='-ml-12 cursor-pointer 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} /> <i className='hover:text-black transform duration-200 text-gray-400 dark:text-gray-300 cursor-pointer fas fa-times' onClick={cleanSearch} />
</div> </div>
)} )}
</div> </div>

View File

@@ -40,12 +40,12 @@ export default function SideRight (props) {
className='rounded-full' className='rounded-full'
/> />
</div> </div>
<div className='text-center font-sans text-xl pb-4'>{BLOG.TITLE}</div> <div className='text-center font-sans text-xl pb-4 dark:text-gray-300'>{BLOG.TITLE}</div>
<SocialButton /> <SocialButton />
</Card> </Card>
<Card> <Card>
<MenuButtonGroup {...props}/> <MenuButtonGroup {...props}/>
<SearchInput /> <SearchInput {...props}/>
</Card> </Card>
<Card> <Card>
<div className='ml-2 mb-3 font-sans'> <div className='ml-2 mb-3 font-sans'>

View File

@@ -2,12 +2,12 @@ import Link from 'next/link'
const TagItemMini = ({ tag, selected = false }) => { const TagItemMini = ({ tag, selected = false }) => {
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref> return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<a className={`cursor-pointer inline-block rounded hover:bg-blue-500 hover:text-white duration-200 <a className={`cursor-pointer inline-block rounded hover:bg-blue-400 dark:hover:text-white hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white mr-2 py-0.5 px-1 text-xs whitespace-nowrap
${selected ${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-blue-900' ? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-blue-900'
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-blue-800`}` }> : `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background `}` }>
<div className='font-light dark:text-gray-400'>{selected && <i className='mr-1 fa-tag'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div> <div className='font-light'>{selected && <i className='mr-1 fa-tag'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a> </a>
</Link> </Link>
} }

View File

@@ -90,7 +90,7 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) =>
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/> <SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
{/* 导航栏 */} {/* 导航栏 */}
<div id='sticky-nav' className={`${CONFIG_HEXO.NAV_TYPE !== 'normal' ? 'fixed bg-white' : ' bg-none -mb-10'} bg-opacity-70 text-black w-full top-0 z-20 transform duration-500 font-sans`}> <div id='sticky-nav' className={`${CONFIG_HEXO.NAV_TYPE !== 'normal' ? 'fixed bg-white' : ' bg-none -mb-10'} dark:bg-black dark:bg-opacity-50 dark:text-gray-200 bg-opacity-70 text-black w-full top-0 z-20 transform duration-500 font-sans`}>
<div className='w-full flex justify-between items-center px-4 py-2 shad'> <div className='w-full flex justify-between items-center px-4 py-2 shad'>
<div className='flex'> <div className='flex'>
<Logo/> <Logo/>

View File

@@ -9,6 +9,7 @@ import TopNavBar from './components/TopNavBar'
import SearchInput from './components/SearchInput' import SearchInput from './components/SearchInput'
import BottomMenuBar from './components/BottomMenuBar' import BottomMenuBar from './components/BottomMenuBar'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
/** /**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏 * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -19,9 +20,10 @@ import { useGlobal } from '@/lib/global'
const LayoutBase = props => { const LayoutBase = props => {
const { children, meta, showInfoCard = true, slotRight, slotTop } = props const { children, meta, showInfoCard = true, slotRight, slotTop } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const router = useRouter()
return ( return (
<div id='container' className='bg-white w-full h-full min-h-screen justify-center'> <div className='bg-white dark:bg-gray-800 w-full h-full min-h-screen justify-center dark:text-gray-300'>
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<main id='wrapper' className='flex justify-between w-full h-full mx-auto'> <main id='wrapper' className='flex justify-between w-full h-full mx-auto'>
{/* 桌面端左侧菜单 */} {/* 桌面端左侧菜单 */}
@@ -37,11 +39,11 @@ const LayoutBase = props => {
</div> </div>
{/* 桌面端右侧 */} {/* 桌面端右侧 */}
<div className='hidden xl:block border-l w-96'> <div className='hidden xl:block border-l dark:border-gray-500 w-96'>
<Tabs className='py-14 px-6 sticky top-0'> <Tabs className='py-14 px-6 sticky top-0'>
{slotRight} {slotRight}
<div key={locale.NAV.ABOUT}> <div key={locale.NAV.ABOUT}>
<SearchInput className='mt-6 mb-12' /> {router.pathname !== '/search' && <SearchInput className='mt-6 mb-12' />}
{showInfoCard && <InfoCard />} {showInfoCard && <InfoCard />}
{CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps />} {CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps />}
</div> </div>

View File

@@ -4,21 +4,30 @@ import { useGlobal } from '@/lib/global'
import TagGroups from './components/TagGroups' import TagGroups from './components/TagGroups'
import CategoryGroup from './components/CategoryGroup' import CategoryGroup from './components/CategoryGroup'
import BlogPostListScroll from './components/BlogPostListScroll' import BlogPostListScroll from './components/BlogPostListScroll'
import { useEffect } from 'react'
export const LayoutSearch = (props) => { export const LayoutSearch = (props) => {
const { locale } = useGlobal() const { locale } = useGlobal()
const { keyword } = props
useEffect(() => {
setTimeout(() => {
const container = document.getElementById('container')
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}> return <LayoutBase {...props}>
<div className='py-12'> <div className='py-12'>
<div className='pb-4 w-full'> <div className='pb-4 w-full'>{locale.NAV.SEARCH}</div>
{locale.NAV.SEARCH} <SearchInput currentSearch={keyword} {...props}/>
</div>
<SearchInput/>
<TagGroups {...props}/> <TagGroups {...props}/>
<CategoryGroup {...props}/> <CategoryGroup {...props}/>
</div> </div>
<div id='container'>
<BlogPostListScroll {...props}/> <BlogPostListScroll {...props}/>
</div>
</LayoutBase> </LayoutBase>
} }

View File

@@ -83,7 +83,7 @@ export const LayoutSlug = props => {
showInfoCard={true} showInfoCard={true}
slotRight={slotRight} slotRight={slotRight}
> >
<h1 className="text-4xl pt-12 font-sans">{post?.title}</h1> <h1 className="text-4xl pt-12 font-sans dark:text-gray-100">{post?.title}</h1>
<section className="flex py-4 items-center font-sans px-1"> <section className="flex py-4 items-center font-sans px-1">
<Link href="/about" passHref> <Link href="/about" passHref>
<> <>

View File

@@ -62,7 +62,7 @@ const Catalog = ({ toc }) => {
<a <a
key={id} key={id}
href={`#${id}`} href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-300
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `} notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
> >
<span <span

View File

@@ -17,8 +17,8 @@ const InfoCard = () => {
className='rounded-full' className='rounded-full'
/> />
</div> </div>
<div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.AUTHOR}</div> <div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-300'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center'>{BLOG.BIO}</div> <div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-400'>{BLOG.BIO}</div>
<SocialButton/> <SocialButton/>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ export default function LogoBar () {
return <div id='top-wrapper' className='w-full max-w-5xl justify-center mx-auto font-sans'> return <div id='top-wrapper' className='w-full max-w-5xl justify-center mx-auto font-sans'>
<div className='w-full'> <div className='w-full'>
<Link href='/'> <Link href='/'>
<a className='text-2xl'>{BLOG.TITLE}</a> <a className='text-2xl dark:text-gray-200'>{BLOG.TITLE}</a>
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,9 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useImperativeHandle, useRef, useState } from 'react' import { useImperativeHandle, useRef, useState } from 'react'
let lock = false
const SearchInput = ({ currentTag, currentSearch, cRef, className }) => { const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
const [searchKey, setSearchKey] = useState(currentSearch || getSearchKey() || '') // const [searchKey, setSearchKey] = useState(currentSearch || getSearchKey() || '')
const [onLoading, setLoadingState] = useState(false) const [onLoading, setLoadingState] = useState(false)
const router = useRouter() const router = useRouter()
const searchInputRef = useRef() const searchInputRef = useRef()
@@ -14,12 +15,15 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
} }
}) })
const handleSearch = (key) => { const handleSearch = () => {
const key = searchInputRef.current.value
if (key && key !== '') { if (key && key !== '') {
setLoadingState(true) setLoadingState(true)
router.push({ pathname: '/search', query: { s: key } }).then(r => { // router.push({ pathname: '/search/' + key }).then(r => {
setLoadingState(false) // setLoadingState(false)
}) // })
location.href = '/search/' + key
} else { } else {
router.push({ pathname: '/' }).then(r => { router.push({ pathname: '/' }).then(r => {
}) })
@@ -34,13 +38,19 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
} }
const cleanSearch = () => { const cleanSearch = () => {
searchInputRef.current.value = '' searchInputRef.current.value = ''
setSearchKey('')
} }
let lock = false const [showClean, setShowClean] = useState(false)
const updateSearchKey = (val) => { const updateSearchKey = (val) => {
if (!lock) { if (lock) {
setSearchKey(val) return
}
searchInputRef.current.value = val
if (val) {
setShowClean(true)
} else {
setShowClean(false)
} }
} }
function lockSearchInput () { function lockSearchInput () {
@@ -61,28 +71,20 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
onCompositionUpdate={lockSearchInput} onCompositionUpdate={lockSearchInput}
onCompositionEnd={unLockSearchInput} onCompositionEnd={unLockSearchInput}
onChange={e => updateSearchKey(e.target.value)} onChange={e => updateSearchKey(e.target.value)}
defaultValue={searchKey} defaultValue={currentSearch}
/> />
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2' <div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}> onClick={handleSearch}>
<i className={`hover:text-black transform duration-200 text-gray-500 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'} `} /> <i className={`hover:text-black transform duration-200 text-gray-500 dark:hover:text-gray-300 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'} `} />
</div> </div>
{(searchKey && searchKey.length && {(showClean &&
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'> <div className='-ml-12 cursor-pointer float-right items-center justify-center py-2'>
<i className='fas fa-times hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} /> <i className='fas fa-times hover:text-black transform duration-200 text-gray-400 cursor-pointer dark:hover:text-gray-300' onClick={cleanSearch} />
</div> </div>
)} )}
</div> </div>
} }
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}
export default SearchInput export default SearchInput

View File

@@ -12,7 +12,7 @@ export default function TopNavBar (props) {
const router = useRouter() const router = useRouter()
return <div id='top-nav' className={'sticky top-0 lg:relative w-full z-50 ' + className}> return <div id='top-nav' className={'sticky top-0 lg:relative w-full z-50 ' + className}>
<div className='flex w-full h-12 shadow bg-white px-5 items-between'> <div className='flex w-full h-12 shadow bg-white dark:bg-gray-900 px-5 items-between'>
<LogoBar /> <LogoBar />
{/* 顶部菜单 */} {/* 顶部菜单 */}
@@ -21,8 +21,8 @@ export default function TopNavBar (props) {
if (link.show) { if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} > return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
<a className={'px-5 duration-300 text-base justify-between hover:underline cursor-pointer flex flex-nowrap items-center ' + <a className={'px-5 duration-300 text-base justify-between dark:text-gray-300 hover:underline cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600 ')} > (selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')} >
<div className='items-center justify-center flex '> <div className='items-center justify-center flex '>
<i className={link.icon} /> <i className={link.icon} />
<div className='ml-4 whitespace-nowrap'>{link.name}</div> <div className='ml-4 whitespace-nowrap'>{link.name}</div>

View File

@@ -2,11 +2,21 @@ import LayoutBase from './LayoutBase'
import StickyBar from './components/StickyBar' import StickyBar from './components/StickyBar'
import BlogPostListScroll from './components/BlogPostListScroll' import BlogPostListScroll from './components/BlogPostListScroll'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { useEffect } from 'react'
export const LayoutSearch = (props) => { export const LayoutSearch = (props) => {
const { locale } = useGlobal() const { locale } = useGlobal()
const { posts } = props const { posts, keyword } = props
useEffect(() => {
setTimeout(() => {
const container = document.getElementById('container')
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 ( return (
<LayoutBase {...props} > <LayoutBase {...props} >
<StickyBar> <StickyBar>

View File

@@ -12,8 +12,8 @@ const BlogPostCard = ({ post, showSummary }) => {
const { locale } = useGlobal() const { locale } = useGlobal()
const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap
return ( return (
<Card className='w-full'> <Card className='w-full animate__animated animate__fadeIn'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'> <div key={post.id} className='flex flex-col-reverse justify-between duration-300'>
<div className='lg:p-8 p-4 flex flex-col w-full'> <div className='lg:p-8 p-4 flex flex-col w-full'>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref> <Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
@@ -47,9 +47,8 @@ const BlogPostCard = ({ post, showSummary }) => {
{/* 搜索结果 */} {/* 搜索结果 */}
{post.results && <p className='mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'> {post.results && <p className='mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
{post.results.map(r => <span key={r}><span dangerouslySetInnerHTML={{ __html: r }}/>...</span>)} {post.results.map(r => <span key={r}>{r}</span>)}
</p> </p> }
}
{showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'> {showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'>
<NotionRenderer <NotionRenderer

View File

@@ -1,18 +1,18 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme' import { loadDarkModeFromCookies, saveDarkModeToCookies } from '@/lib/theme'
const DarkModeButton = () => { const DarkModeButton = () => {
const { changeTheme } = useGlobal() const { updateDarkMode } = useGlobal()
const userTheme = loadUserThemeFromCookies() const isDarkMode = loadDarkModeFromCookies()
// 用户手动设置主题 // 用户手动设置主题
const handleChangeDarkMode = () => { const handleChangeDarkMode = () => {
const newTheme = (userTheme === 'light' ? 'dark' : 'light') const newTheme = (isDarkMode ? 'dark' : 'light')
saveTheme(newTheme) saveDarkModeToCookies(newTheme)
changeTheme(newTheme) updateDarkMode(newTheme)
} }
return <div className='z-10 duration-200 text-xs cursor-pointer py-1.5 px-1'> return <div className='z-10 duration-200 text-xs cursor-pointer py-1.5 px-1'>
<i id='darkModeButton' className={`hover:scale-125 transform duration-200 fas ${userTheme === 'dark' ? 'fa-sun' : 'fa-moon'}`} <i id='darkModeButton' className={`hover:scale-125 transform duration-200 fas ${isDarkMode ? 'fa-sun' : 'fa-moon'}`}
onClick={handleChangeDarkMode} /> onClick={handleChangeDarkMode} />
</div> </div>
} }

View File

@@ -1,5 +1,5 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme' import { saveDarkModeToCookies } from '@/lib/theme'
import CONFIG_NEXT from '../config_next' import CONFIG_NEXT from '../config_next'
export default function FloatDarkModeButton () { export default function FloatDarkModeButton () {
@@ -7,16 +7,15 @@ export default function FloatDarkModeButton () {
return <></> return <></>
} }
const { changeTheme } = useGlobal() const { isDarkMode, updateDarkMode } = useGlobal()
const userTheme = loadUserThemeFromCookies()
// 用户手动设置主题 // 用户手动设置主题
const handleChangeDarkMode = () => { const handleChangeDarkMode = () => {
const newTheme = userTheme === 'light' ? 'dark' : 'light' const newStatus = !isDarkMode
saveTheme(newTheme) saveDarkModeToCookies(newStatus)
changeTheme(newTheme) updateDarkMode(newStatus)
const htmlElement = document.getElementsByTagName('html')[0] const htmlElement = document.getElementsByTagName('html')[0]
htmlElement.classList?.remove(userTheme) htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
htmlElement.classList?.add(newTheme) htmlElement.classList?.add(newStatus ? 'dark' : 'light')
} }
return ( return (
@@ -27,7 +26,7 @@ export default function FloatDarkModeButton () {
> >
<i <i
id="darkModeButton" id="darkModeButton"
className={`${userTheme === 'dark' ? 'fa-sun' : 'fa-moon'} fas hover:scale-150 transform duration-200`} className={`${isDarkMode ? 'fa-sun' : 'fa-moon'} fas hover:scale-150 transform duration-200`}
/> />
</div> </div>
) )

View File

@@ -27,7 +27,7 @@ export default function Header () {
) )
} }
}) })
const { theme } = useGlobal() const { isDarkMode } = useGlobal()
const autoScrollEnd = () => { const autoScrollEnd = () => {
if (autoScroll) { if (autoScroll) {
@@ -61,7 +61,7 @@ export default function Header () {
} }
const updateTopNav = () => { const updateTopNav = () => {
if (theme !== 'dark') { if (!isDarkMode) {
const stickyNavElement = document.getElementById('sticky-nav') const stickyNavElement = document.getElementById('sticky-nav')
if (window.scrollY < window.innerHeight) { if (window.scrollY < window.innerHeight) {
stickyNavElement.classList.add('dark') stickyNavElement.classList.add('dark')