mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 15:09:22 +00:00
Merge remote-tracking branch 'origin/main' into theme-medium
# Conflicts: # themes/index.js
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import lang from './lang'
|
||||
import { useContext, createContext, useState, useEffect } from 'react'
|
||||
import { useContext, createContext, useState } from 'react'
|
||||
import Router from 'next/router'
|
||||
import { initTheme, loadUserThemeFromCookies } from './theme'
|
||||
import { initDarkMode } from './theme'
|
||||
const GlobalContext = createContext()
|
||||
let hasInit = false
|
||||
|
||||
/**
|
||||
* 全局变量Provider,包括语言本地化、样式主题、搜索词
|
||||
@@ -11,8 +12,8 @@ const GlobalContext = createContext()
|
||||
* @constructor
|
||||
*/
|
||||
export function GlobalContextProvider ({ children }) {
|
||||
const [locale, changeLocale] = useState(generateLocaleDict('en-US'))
|
||||
const [theme, changeTheme] = useState(loadUserThemeFromCookies())
|
||||
const [locale, updateLocale] = useState(generateLocaleDict('en-US'))
|
||||
const [isDarkMode, updateDarkMode] = useState(false)
|
||||
const [onLoading, changeLoadingState] = useState(false)
|
||||
Router.events.on('routeChangeStart', (...args) => {
|
||||
changeLoadingState(true)
|
||||
@@ -23,13 +24,17 @@ export function GlobalContextProvider ({ children }) {
|
||||
})
|
||||
|
||||
// 服务端静态渲染,在渲染hooks后根据前端变量做初始化工作
|
||||
useEffect(() => {
|
||||
initTheme(theme, changeTheme)
|
||||
initLocale(locale, changeLocale)
|
||||
})
|
||||
setTimeout(() => {
|
||||
console.log('初始化', hasInit, isDarkMode)
|
||||
if (!hasInit) {
|
||||
hasInit = true
|
||||
initLocale(locale, updateLocale)
|
||||
initDarkMode(isDarkMode, updateDarkMode)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ onLoading, locale, theme, changeTheme }}>
|
||||
<GlobalContext.Provider value={{ onLoading, locale, isDarkMode, updateDarkMode }}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function getAllPosts ({ notionPageData, from, pageType }) {
|
||||
})
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
console.warn('文章列表为空')
|
||||
const cacheKey = 'page_block_' + BLOG.NOTION_PAGE_ID
|
||||
await delCacheData(cacheKey)
|
||||
}
|
||||
|
||||
48
lib/theme.js
48
lib/theme.js
@@ -1,37 +1,45 @@
|
||||
import cookie from 'react-cookies'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
* @param theme 用户默认主题state
|
||||
* @param changeTheme 更改主题ChangeState函数
|
||||
* @param isDarkMode
|
||||
* @param updateDarkMode 更改主题ChangeState函数
|
||||
* @description 读取cookie中存的用户主题
|
||||
*/
|
||||
export const initTheme = (theme, changeTheme) => {
|
||||
// 若未指定主题,则从时间和浏览器偏好中决定初始主题
|
||||
if (!theme) {
|
||||
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'
|
||||
}
|
||||
export const initDarkMode = (isDarkMode, updateDarkMode) => {
|
||||
if (!isDarkMode) {
|
||||
isDarkMode = isPreferDark()
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const htmlElement = document.getElementsByTagName('html')
|
||||
htmlElement.className = ''
|
||||
changeTheme(theme)
|
||||
saveTheme(theme)
|
||||
htmlElement.classList?.add(theme)
|
||||
updateDarkMode(isDarkMode)
|
||||
saveDarkModeToCookies(isDarkMode)
|
||||
document.getElementsByTagName('html')[0].setAttribute('class', isDarkMode ? 'dark' : 'light')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否优先深色模式
|
||||
* @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 {*}
|
||||
*/
|
||||
export const loadUserThemeFromCookies = () => {
|
||||
export const loadDarkModeFromCookies = () => {
|
||||
return cookie.load('theme')
|
||||
}
|
||||
|
||||
@@ -39,6 +47,6 @@ export const loadUserThemeFromCookies = () => {
|
||||
* 保存默认主题
|
||||
* @param newTheme
|
||||
*/
|
||||
export const saveTheme = (newTheme) => {
|
||||
export const saveDarkModeToCookies = (newTheme) => {
|
||||
cookie.save('theme', newTheme, { path: '/' })
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class MyDocument extends Document {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Html lang={BLOG.LANG}>
|
||||
<Html lang={BLOG.LANG} className='test'>
|
||||
<Head>
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
<link rel='icon' href='/favicon.svg' type='image/svg+xml' />
|
||||
|
||||
@@ -4,6 +4,11 @@ import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getDataFromCache } from '@/lib/cache/cache_manager'
|
||||
|
||||
/**
|
||||
* 服务端搜索
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export async function getServerSideProps ({ params: { keyword } }) {
|
||||
const {
|
||||
allPosts,
|
||||
@@ -110,18 +115,16 @@ async function filterByMemCache (allPosts, keyword) {
|
||||
// console.log('搜索是否命中缓存', page !== null, indexContent)
|
||||
post.results = []
|
||||
let hitCount = 0
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
for (const i in indexContent) {
|
||||
const c = indexContent[i]
|
||||
const index = c.toLowerCase().indexOf(keyword.toLowerCase())
|
||||
const referText = c?.replace(re, `<span class='text-red-500'>${keyword}</span>`)
|
||||
if (index > -1) {
|
||||
hit = true
|
||||
hitCount += 1
|
||||
post.results.push(`<span>${referText}</span>`)
|
||||
post.results.push(c)
|
||||
} else {
|
||||
if ((post.results.length - 1) / hitCount < 3 || i === 0) {
|
||||
post.results.push(`<span>${referText}</span>`)
|
||||
post.results.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 浏览器前端搜索
|
||||
*/
|
||||
export async function getStaticProps () {
|
||||
const {
|
||||
allPosts,
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
import BlogListPage from './components/BlogListPage'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
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}/>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Catalog from './Catalog'
|
||||
|
||||
function AsideLeft (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 />
|
||||
|
||||
<section className='flex flex-col text-gray-600'>
|
||||
@@ -19,11 +19,11 @@ function AsideLeft (props) {
|
||||
|
||||
<section className='flex flex-col text-gray-600'>
|
||||
<hr className='w-12 my-8' />
|
||||
<SearchInput/>
|
||||
<SearchInput {...props}/>
|
||||
</section>
|
||||
|
||||
<section className='flex flex-col'>
|
||||
<hr className='w-12 my-8 ' />
|
||||
<section className='flex flex-col dark:text-gray-300'>
|
||||
<hr className='w-12 my-8' />
|
||||
{ BLOG.DESCRIPTION }
|
||||
</section>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const Catalog = ({ toc }) => {
|
||||
}, throttleMs))
|
||||
|
||||
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'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
@@ -59,7 +59,7 @@ const Catalog = ({ toc }) => {
|
||||
<a
|
||||
key={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} `}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -24,10 +24,10 @@ function GroupMenu ({ customNav }) {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === 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' : ' ')} >
|
||||
<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>
|
||||
{link.slot}
|
||||
</a>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link'
|
||||
function Logo () {
|
||||
return <section className='flex'>
|
||||
<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>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
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) }}>
|
||||
<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>
|
||||
|
||||
{(searchKey && searchKey.length &&
|
||||
|
||||
@@ -38,7 +38,7 @@ const LayoutBase = (props) => {
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<div className='bg-white'>
|
||||
return (<div className='bg-white dark:bg-gray-900'>
|
||||
<CommonHead meta={meta} />
|
||||
<TopNav {...props}/>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const LayoutCategoryIndex = props => {
|
||||
}
|
||||
return (
|
||||
<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">
|
||||
<i className="mr-4 fas fa-th" />
|
||||
{locale.COMMON.CATEGORY}:
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSearch = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
<BlogPostListPage {...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}>
|
||||
<div ic='container'>
|
||||
<BlogPostListPage {...props}/>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const LayoutSlug = props => {
|
||||
showTag={false}
|
||||
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} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 ">
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased bg-white dark:bg-gray-800" >
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-5'>
|
||||
|
||||
@@ -9,7 +9,7 @@ import CONFIG_HEXO from '../config_hexo'
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
|
||||
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 className='lg:p-8 p-4 flex flex-col w-full'>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{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}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ const Catalog = ({ toc }) => {
|
||||
<a
|
||||
key={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} `}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -14,7 +14,7 @@ let autoScroll = false
|
||||
*/
|
||||
export default function Header () {
|
||||
const [typed, changeType] = useState()
|
||||
const { theme } = useGlobal()
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
useEffect(() => {
|
||||
scrollTrigger()
|
||||
@@ -76,7 +76,7 @@ export default function Header () {
|
||||
}
|
||||
|
||||
const updateTopNav = () => {
|
||||
if (theme !== 'dark') {
|
||||
if (!isDarkMode) {
|
||||
const stickyNavElement = document.getElementById('sticky-nav')
|
||||
if (window.scrollY < window.innerHeight) {
|
||||
stickyNavElement?.classList?.add('dark')
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function HeaderArticle ({ post }) {
|
||||
</div>
|
||||
|
||||
<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 && <>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer mr-2 dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
|
||||
@@ -8,7 +8,7 @@ const MenuButtonGroup = (props) => {
|
||||
const { postCount, customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
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 = [
|
||||
{ icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
|
||||
@@ -52,7 +52,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
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}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
@@ -61,14 +61,14 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
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) }}>
|
||||
<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>
|
||||
|
||||
{(searchKey && searchKey.length &&
|
||||
<div className='-ml-12 cursor-pointer 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 className='-ml-12 cursor-pointer float-right items-center justify-center py-2'>
|
||||
<i className='hover:text-black transform duration-200 text-gray-400 dark:text-gray-300 cursor-pointer fas fa-times' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -40,12 +40,12 @@ export default function SideRight (props) {
|
||||
className='rounded-full'
|
||||
/>
|
||||
</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 />
|
||||
</Card>
|
||||
<Card>
|
||||
<MenuButtonGroup {...props}/>
|
||||
<SearchInput />
|
||||
<SearchInput {...props}/>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className='ml-2 mb-3 font-sans'>
|
||||
|
||||
@@ -2,12 +2,12 @@ import Link from 'next/link'
|
||||
|
||||
const TagItemMini = ({ tag, selected = false }) => {
|
||||
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
|
||||
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
|
||||
<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
|
||||
${selected
|
||||
? '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`}` }>
|
||||
<div className='font-light dark:text-gray-400'>{selected && <i className='mr-1 fa-tag'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background `}` }>
|
||||
<div className='font-light'>{selected && <i className='mr-1 fa-tag'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) =>
|
||||
<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='flex'>
|
||||
<Logo/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import TopNavBar from './components/TopNavBar'
|
||||
import SearchInput from './components/SearchInput'
|
||||
import BottomMenuBar from './components/BottomMenuBar'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
@@ -19,9 +20,10 @@ import { useGlobal } from '@/lib/global'
|
||||
const LayoutBase = props => {
|
||||
const { children, meta, showInfoCard = true, slotRight, slotTop } = props
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
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} />
|
||||
<main id='wrapper' className='flex justify-between w-full h-full mx-auto'>
|
||||
{/* 桌面端左侧菜单 */}
|
||||
@@ -37,11 +39,11 @@ const LayoutBase = props => {
|
||||
</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'>
|
||||
{slotRight}
|
||||
<div key={locale.NAV.ABOUT}>
|
||||
<SearchInput className='mt-6 mb-12' />
|
||||
{router.pathname !== '/search' && <SearchInput className='mt-6 mb-12' />}
|
||||
{showInfoCard && <InfoCard />}
|
||||
{CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && <RevolverMaps />}
|
||||
</div>
|
||||
|
||||
@@ -4,21 +4,30 @@ import { useGlobal } from '@/lib/global'
|
||||
import TagGroups from './components/TagGroups'
|
||||
import CategoryGroup from './components/CategoryGroup'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const LayoutSearch = (props) => {
|
||||
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}>
|
||||
|
||||
<div className='py-12'>
|
||||
<div className='pb-4 w-full'>
|
||||
{locale.NAV.SEARCH}
|
||||
</div>
|
||||
<SearchInput/>
|
||||
|
||||
<div className='pb-4 w-full'>{locale.NAV.SEARCH}</div>
|
||||
<SearchInput currentSearch={keyword} {...props}/>
|
||||
<TagGroups {...props}/>
|
||||
<CategoryGroup {...props}/>
|
||||
</div>
|
||||
|
||||
<BlogPostListScroll {...props}/>
|
||||
<div id='container'>
|
||||
<BlogPostListScroll {...props}/>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export const LayoutSlug = props => {
|
||||
showInfoCard={true}
|
||||
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">
|
||||
<Link href="/about" passHref>
|
||||
<>
|
||||
|
||||
@@ -62,7 +62,7 @@ const Catalog = ({ toc }) => {
|
||||
<a
|
||||
key={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} `}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -17,8 +17,8 @@ const InfoCard = () => {
|
||||
className='rounded-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center'>{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='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 dark:text-gray-400'>{BLOG.BIO}</div>
|
||||
<SocialButton/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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'>
|
||||
<div className='w-full'>
|
||||
<Link href='/'>
|
||||
<a className='text-2xl'>{BLOG.TITLE}</a>
|
||||
<a className='text-2xl dark:text-gray-200'>{BLOG.TITLE}</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
let lock = false
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
const [searchKey, setSearchKey] = useState(currentSearch || getSearchKey() || '')
|
||||
// const [searchKey, setSearchKey] = useState(currentSearch || getSearchKey() || '')
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
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 !== '') {
|
||||
setLoadingState(true)
|
||||
router.push({ pathname: '/search', query: { s: key } }).then(r => {
|
||||
setLoadingState(false)
|
||||
})
|
||||
// router.push({ pathname: '/search/' + key }).then(r => {
|
||||
// setLoadingState(false)
|
||||
// })
|
||||
location.href = '/search/' + key
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {
|
||||
})
|
||||
@@ -34,13 +38,19 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
}
|
||||
const cleanSearch = () => {
|
||||
searchInputRef.current.value = ''
|
||||
setSearchKey('')
|
||||
}
|
||||
|
||||
let lock = false
|
||||
const [showClean, setShowClean] = useState(false)
|
||||
const updateSearchKey = (val) => {
|
||||
if (!lock) {
|
||||
setSearchKey(val)
|
||||
if (lock) {
|
||||
return
|
||||
}
|
||||
searchInputRef.current.value = val
|
||||
|
||||
if (val) {
|
||||
setShowClean(true)
|
||||
} else {
|
||||
setShowClean(false)
|
||||
}
|
||||
}
|
||||
function lockSearchInput () {
|
||||
@@ -61,28 +71,20 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
onCompositionEnd={unLockSearchInput}
|
||||
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'
|
||||
onClick={() => { handleSearch(searchKey) }}>
|
||||
<i className={`hover:text-black transform duration-200 text-gray-500 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'} `} />
|
||||
<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 dark:hover:text-gray-300 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'} `} />
|
||||
</div>
|
||||
|
||||
{(searchKey && searchKey.length &&
|
||||
<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='fas fa-times hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
|
||||
{(showClean &&
|
||||
<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 dark:hover:text-gray-300' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function getSearchKey () {
|
||||
const router = useRouter()
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function TopNavBar (props) {
|
||||
const router = useRouter()
|
||||
|
||||
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 />
|
||||
|
||||
{/* 顶部菜单 */}
|
||||
@@ -21,8 +21,8 @@ export default function TopNavBar (props) {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === 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 ' +
|
||||
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600 ')} >
|
||||
<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')} >
|
||||
<div className='items-center justify-center flex '>
|
||||
<i className={link.icon} />
|
||||
<div className='ml-4 whitespace-nowrap'>{link.name}</div>
|
||||
|
||||
@@ -2,11 +2,21 @@ import LayoutBase from './LayoutBase'
|
||||
import StickyBar from './components/StickyBar'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const LayoutSearch = (props) => {
|
||||
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 (
|
||||
<LayoutBase {...props} >
|
||||
<StickyBar>
|
||||
|
||||
@@ -12,8 +12,8 @@ const BlogPostCard = ({ post, showSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse justify-between duration-300'>
|
||||
<Card className='w-full animate__animated animate__fadeIn'>
|
||||
<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'>
|
||||
<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.map(r => <span key={r}><span dangerouslySetInnerHTML={{ __html: r }}/>...</span>)}
|
||||
</p>
|
||||
}
|
||||
{post.results.map(r => <span key={r}>{r}</span>)}
|
||||
</p> }
|
||||
|
||||
{showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'>
|
||||
<NotionRenderer
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
import { loadDarkModeFromCookies, saveDarkModeToCookies } from '@/lib/theme'
|
||||
|
||||
const DarkModeButton = () => {
|
||||
const { changeTheme } = useGlobal()
|
||||
const userTheme = loadUserThemeFromCookies()
|
||||
const { updateDarkMode } = useGlobal()
|
||||
const isDarkMode = loadDarkModeFromCookies()
|
||||
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newTheme = (userTheme === 'light' ? 'dark' : 'light')
|
||||
saveTheme(newTheme)
|
||||
changeTheme(newTheme)
|
||||
const newTheme = (isDarkMode ? 'dark' : 'light')
|
||||
saveDarkModeToCookies(newTheme)
|
||||
updateDarkMode(newTheme)
|
||||
}
|
||||
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} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
import { saveDarkModeToCookies } from '@/lib/theme'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
export default function FloatDarkModeButton () {
|
||||
@@ -7,16 +7,15 @@ export default function FloatDarkModeButton () {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const { changeTheme } = useGlobal()
|
||||
const userTheme = loadUserThemeFromCookies()
|
||||
const { isDarkMode, updateDarkMode } = useGlobal()
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newTheme = userTheme === 'light' ? 'dark' : 'light'
|
||||
saveTheme(newTheme)
|
||||
changeTheme(newTheme)
|
||||
const newStatus = !isDarkMode
|
||||
saveDarkModeToCookies(newStatus)
|
||||
updateDarkMode(newStatus)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove(userTheme)
|
||||
htmlElement.classList?.add(newTheme)
|
||||
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -27,7 +26,7 @@ export default function FloatDarkModeButton () {
|
||||
>
|
||||
<i
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Header () {
|
||||
)
|
||||
}
|
||||
})
|
||||
const { theme } = useGlobal()
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
const autoScrollEnd = () => {
|
||||
if (autoScroll) {
|
||||
@@ -61,7 +61,7 @@ export default function Header () {
|
||||
}
|
||||
|
||||
const updateTopNav = () => {
|
||||
if (theme !== 'dark') {
|
||||
if (!isDarkMode) {
|
||||
const stickyNavElement = document.getElementById('sticky-nav')
|
||||
if (window.scrollY < window.innerHeight) {
|
||||
stickyNavElement.classList.add('dark')
|
||||
|
||||
Reference in New Issue
Block a user