diff --git a/.env.local b/.env.local index fc777115..1283ba60 100644 --- a/.env.local +++ b/.env.local @@ -1,2 +1,2 @@ # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables -NEXT_PUBLIC_VERSION=3.16.4 \ No newline at end of file +NEXT_PUBLIC_VERSION=4.0.0 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 4b95adcd..bab5ea27 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,7 @@ module.exports = { } }, rules: { + 'react/no-unknown-property': 'off', // + + ) +} diff --git a/components/FullScreenButton.js b/components/FullScreenButton.js new file mode 100644 index 00000000..053de666 --- /dev/null +++ b/components/FullScreenButton.js @@ -0,0 +1,48 @@ +import { isBrowser } from '@/lib/utils' +import React, { useState } from 'react' + +/** + * 全屏按钮 + * @returns + */ +const FullScreenButton = () => { + const [isFullScreen, setIsFullScreen] = useState(false) + + const handleFullScreenClick = () => { + if (!isBrowser()) { + return + } + const element = document.documentElement + if (!isFullScreen) { + if (element.requestFullscreen) { + element.requestFullscreen() + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen() + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen() + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen() + } + setIsFullScreen(true) + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } else if (document.msExitFullscreen) { + document.msExitFullscreen() + } + setIsFullScreen(false) + } + } + + return ( + + ) +} + +export default FullScreenButton diff --git a/components/GoogleAdsense.js b/components/GoogleAdsense.js index 83ffc006..d9718369 100644 --- a/components/GoogleAdsense.js +++ b/components/GoogleAdsense.js @@ -2,6 +2,10 @@ import BLOG from '@/blog.config' import { useRouter } from 'next/router' import { useEffect } from 'react' +/** + * 初始化谷歌广告 + * @returns + */ export default function GoogleAdsense() { const initGoogleAdsense = () => { setTimeout(() => { @@ -21,12 +25,8 @@ export default function GoogleAdsense() { } const router = useRouter() - useEffect(() => { - router.events.on('routeChangeComplete', initGoogleAdsense) - return () => { - router.events.off('routeChangeComplete', initGoogleAdsense) - } + initGoogleAdsense() }, [router]) return null diff --git a/components/HeroIcons.js b/components/HeroIcons.js new file mode 100644 index 00000000..6a24d8b7 --- /dev/null +++ b/components/HeroIcons.js @@ -0,0 +1,94 @@ +/** + * @see https://heroicons.com/ + * @returns + */ + +export const Moon = () => { + return + + +} + +export const Sun = () => { + return + + +} + +export const Home = ({ className }) => { + return + + +} + +export const User = ({ className }) => { + return + + +} + +export const ArrowPath = ({ className }) => { + return + + +} + +export const ChevronLeft = ({ className }) => { + return + + +} + +export const ChevronRight = ({ className }) => { + return + + +} + +export const ChevronDoubleRight = ({ className }) => { + return + + +} + +export const InformationCircle = ({ className }) => { + return + + +} + +export const HashTag = ({ className }) => { + return + + +} + +export const GlobeAlt = ({ className }) => { + return + + +} + +export const ArrowRightCircle = ({ className }) => { + return + + +} + +export const PlusSmall = ({ className }) => { + return + + +} + +export const ArrowSmallRight = ({ className }) => { + return + + +} + +export const ArrowSmallUp = ({ className }) => { + return + + +} diff --git a/components/NotionPage.js b/components/NotionPage.js index e389468f..e559bdbb 100644 --- a/components/NotionPage.js +++ b/components/NotionPage.js @@ -1,12 +1,14 @@ import { NotionRenderer } from 'react-notion-x' import dynamic from 'next/dynamic' -// import mediumZoom from '@fisch0920/medium-zoom' -import React, { useEffect } from 'react' +import mediumZoom from '@fisch0920/medium-zoom' +import React, { useEffect, useRef } from 'react' // import { Code } from 'react-notion-x/build/third-party/code' import TweetEmbed from 'react-tweet-embed' import 'katex/dist/katex.min.css' import { mapImgUrl } from '@/lib/notion/mapImage' +import BLOG from '@/blog.config' +import { isBrowser } from '@/lib/utils' const Code = dynamic(() => import('react-notion-x/build/third-party/code').then(async (m) => { @@ -21,6 +23,7 @@ const Equation = dynamic(() => return m.Equation }), { ssr: false } ) + const Pdf = dynamic( () => import('react-notion-x/build/third-party/pdf').then((m) => m.Pdf), { @@ -51,11 +54,39 @@ const NotionPage = ({ post, className }) => { autoScrollToTarget() }, []) + const zoom = typeof window !== 'undefined' && mediumZoom({ + container: '.notion-viewport', + background: 'rgba(0, 0, 0, 0.2)', + margin: getMediumZoomMargin() + }) + const zoomRef = useRef(zoom ? zoom.clone() : null) + + useEffect(() => { + // 将相册gallery下的图片加入放大功能 + if (JSON.parse(BLOG.POST_DISABLE_GALLERY_CLICK)) { + setTimeout(() => { + if (isBrowser()) { + const imgList = document?.querySelectorAll('.notion-collection-card-cover img') + if (imgList && zoomRef.current) { + for (let i = 0; i < imgList.length; i++) { + (zoomRef.current).attach(imgList[i]) + } + } + + const cards = document.getElementsByClassName('notion-collection-card') + for (const e of cards) { + e.removeAttribute('href') + } + } + }, 800) + } + }, []) + if (!post || !post.blockMap) { return <>{post?.summary || ''} } - return
+ return
{ return '/' + id.replace(/-/g, '') } +/** + * 缩放 + * @returns + */ +function getMediumZoomMargin() { + const width = window.innerWidth + + if (width < 500) { + return 8 + } else if (width < 800) { + return 20 + } else if (width < 1280) { + return 30 + } else if (width < 1600) { + return 40 + } else if (width < 1920) { + return 48 + } else { + return 72 + } +} export default NotionPage diff --git a/components/PrismMac.js b/components/PrismMac.js index 01f4e65b..9e684d6a 100644 --- a/components/PrismMac.js +++ b/components/PrismMac.js @@ -15,6 +15,7 @@ import { loadExternalResource } from '@/lib/utils' import { useRouter } from 'next/navigation' /** + * 代码美化相关 * @author https://github.com/txs/ * @returns */ @@ -31,11 +32,59 @@ const PrismMac = () => { } renderPrismMac() renderMermaid() + renderCollapseCode() }) }, [router]) return <> } +/** + * 将代码块转为可折叠对象 + */ +const renderCollapseCode = () => { + if (!JSON.parse(BLOG.CODE_COLLAPSE)) { + return + } + const codeBlocks = document.querySelectorAll('.code-toolbar') + for (const codeBlock of codeBlocks) { + // 判断当前元素是否被包裹 + if (codeBlock.closest('.collapse-wrapper')) { + continue // 如果被包裹了,跳过当前循环 + } + + const code = codeBlock.querySelector('code') + const language = code.getAttribute('class').match(/language-(\w+)/)[1] + + const collapseWrapper = document.createElement('div') + collapseWrapper.className = 'collapse-wrapper w-full py-2' + const panelWrapper = document.createElement('div') + panelWrapper.className = 'border rounded-md border-indigo-500' + + const header = document.createElement('div') + header.className = 'flex justify-between items-center px-4 py-2 cursor-pointer select-none' + header.innerHTML = `

${language}

` + + const panel = document.createElement('div') + panel.className = 'invisible h-0 transition-transform duration-200 border-t border-gray-300' + + panelWrapper.appendChild(header) + panelWrapper.appendChild(panel) + collapseWrapper.appendChild(panelWrapper) + + codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock) + panel.appendChild(codeBlock) + + header.addEventListener('click', () => { + panel.classList.toggle('invisible') + panel.classList.toggle('h-0') + panel.classList.toggle('h-auto') + header.querySelector('svg').classList.toggle('rotate-180') + panelWrapper.classList.toggle('border-gray-300') + panelWrapper.classList.toggle('border-indigo-500') + }) + } +} + /** * 将mermaid语言 渲染成图片 */ diff --git a/components/ShareBar.js b/components/ShareBar.js index b55c4dc9..caf03666 100644 --- a/components/ShareBar.js +++ b/components/ShareBar.js @@ -14,15 +14,15 @@ const ShareBar = ({ post }) => { return
- +
} diff --git a/components/ThemeSwitch.js b/components/ThemeSwitch.js index c0e4b1c2..fc875dae 100644 --- a/components/ThemeSwitch.js +++ b/components/ThemeSwitch.js @@ -1,8 +1,9 @@ import { useGlobal } from '@/lib/global' import React from 'react' import { Draggable } from './Draggable' -import { ALL_THEME } from '@/themes/theme' +import { THEMES } from '@/themes/theme' import { useRouter } from 'next/router' +import DarkModeButton from './DarkModeButton' /** * * @returns 主题切换 @@ -22,14 +23,15 @@ const ThemeSwitch = () => { return (<> -
-
- + {THEMES?.map(t => { return })} - +
diff --git a/components/WordCount.js b/components/WordCount.js new file mode 100644 index 00000000..315c58d3 --- /dev/null +++ b/components/WordCount.js @@ -0,0 +1,67 @@ +import { useGlobal } from '@/lib/global' +import { useEffect } from 'react' + +/** + * 字数统计 + * @returns + */ +export default function WordCount() { + const { locale } = useGlobal() + useEffect(() => { + countWords() + }) + + return + + + 0 + + + + + 0 {locale.COMMON.MINUTE} + + +} + +/** + * 更新字数统计和阅读时间 + */ +function countWords() { + const articleText = deleteHtmlTag(document.getElementById('notion-article')?.innerHTML) + const wordCount = fnGetCpmisWords(articleText) + // 阅读速度 300-500每分钟 + document.getElementById('wordCount').innerHTML = wordCount + document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1 + const wordCountWrapper = document.getElementById('wordCountWrapper') + wordCountWrapper.classList.remove('hidden') +} + +// 去除html标签 +function deleteHtmlTag(str) { + if (!str) { + return '' + } + str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合 + return str +} + +// 用word方式计算正文字数 +function fnGetCpmisWords(str) { + if (!str) { + return 0 + } + let sLen = 0 + try { + // eslint-disable-next-line no-irregular-whitespace + str = str.replace(/(\r\n+|\s+| +)/g, '龘') + // eslint-disable-next-line no-control-regex + str = str.replace(/[\x00-\xff]/g, 'm') + str = str.replace(/m+/g, '*') + str = str.replace(/龘+/g, '') + sLen = str.length + } catch (e) { + + } + return sLen +} diff --git a/lib/formatDate.js b/lib/formatDate.js index 0d752555..5eb586bf 100644 --- a/lib/formatDate.js +++ b/lib/formatDate.js @@ -9,10 +9,12 @@ export default function formatDate (date, local) { const d = new Date(date) const options = { year: 'numeric', month: 'short', day: 'numeric' } const res = d.toLocaleDateString(local, options) - return local.slice(0, 2).toLowerCase() === 'zh' + const format = local.slice(0, 2).toLowerCase() === 'zh' ? res.replace('年', '-').replace('月', '-').replace('日', '') : res + return format } + export function formatDateFmt (timestamp, fmt) { const date = new Date(timestamp) const o = { diff --git a/lib/global.js b/lib/global.js index a6e45b3f..95c9614a 100644 --- a/lib/global.js +++ b/lib/global.js @@ -2,7 +2,7 @@ import { generateLocaleDict, initLocale } from './lang' import { createContext, useContext, useEffect, useState } from 'react' import { useRouter } from 'next/router' import BLOG from '@/blog.config' -import { ALL_THEME, initDarkMode } from '@/themes/theme' +import { THEMES, initDarkMode } from '@/themes/theme' import NProgress from 'nprogress' import { getQueryVariable, isBrowser } from './utils' @@ -69,9 +69,9 @@ export function GlobalContextProvider({ children }) { // 切换主题 function switchTheme() { - const currentIndex = ALL_THEME.indexOf(theme) - const newIndex = currentIndex < ALL_THEME.length - 1 ? currentIndex + 1 : 0 - const newTheme = ALL_THEME[newIndex] + const currentIndex = THEMES.indexOf(theme) + const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0 + const newTheme = THEMES[newIndex] const query = { ...router.query, theme: newTheme } router.push({ pathname: router.pathname, query }) return newTheme @@ -105,9 +105,8 @@ const initTheme = () => { if (elements?.length > 1) { elements[elements.length - 1].scrollIntoView() // 删除前面的元素,只保留最后一个元素 - elements[0].parentNode.removeChild(elements[0]) - if (Object.prototype.hasOwnProperty.call(elements, 'pop')) { - elements.pop() + for (let i = 0; i < elements.length - 1; i++) { + elements[i].parentNode.removeChild(elements[i]) } } }, 500) diff --git a/lib/lang/en-US.js b/lib/lang/en-US.js index de277434..7d036425 100644 --- a/lib/lang/en-US.js +++ b/lib/lang/en-US.js @@ -1,7 +1,7 @@ export default { LOCALE: 'en-US', NAV: { - INDEX: 'Blog', + INDEX: 'Home', RSS: 'RSS', SEARCH: 'Search', ABOUT: 'About', @@ -35,6 +35,7 @@ export default { SUBMIT: 'Submit', POST_TIME: 'Post on', LAST_EDITED_TIME: 'Last edited', + COMMENTS: 'Comments', RECENT_COMMENTS: 'Recent Comments', DEBUG_OPEN: 'Debug', DEBUG_CLOSE: 'Close', diff --git a/lib/lang/zh-CN.js b/lib/lang/zh-CN.js index 559afb91..fe130d6f 100644 --- a/lib/lang/zh-CN.js +++ b/lib/lang/zh-CN.js @@ -12,7 +12,7 @@ export default { COMMON: { MORE: '更多', NO_MORE: '没有更多了', - LATEST_POSTS: '最新文章', + LATEST_POSTS: '最新发布', TAGS: '标签', NO_TAG: 'NoTag', CATEGORY: '分类', @@ -37,6 +37,7 @@ export default { SUBMIT: '提交', POST_TIME: '发布于', LAST_EDITED_TIME: '最后更新', + COMMENTS: '评论', RECENT_COMMENTS: '最新评论', DEBUG_OPEN: '开启调试', DEBUG_CLOSE: '关闭调试', @@ -47,8 +48,8 @@ export default { WORD_COUNT: '字数' }, PAGINATION: { - PREV: '上一页', - NEXT: '下一页' + PREV: '上页', + NEXT: '下页' }, SEARCH: { ARTICLES: '搜索文章', diff --git a/lib/mailchimp.js b/lib/mailchimp.js new file mode 100644 index 00000000..1d8bd2e2 --- /dev/null +++ b/lib/mailchimp.js @@ -0,0 +1,49 @@ +import BLOG from '@/blog.config' + +/** +* 订阅邮件-服务端接口 +* @param {*} email +* @returns +*/ +export default function subscribeToMailchimpApi({ email, first_name = '', last_name = '' }) { + const listId = BLOG.MAILCHIMP_LIST_ID // 替换为你的邮件列表 ID + const apiKey = BLOG.MAILCHIMP_API_KEY // 替换为你的 API KEY + if (!email || !listId || !apiKey) { + return {} + } + const data = { + email_address: email, + status: 'subscribed', + merge_fields: { + FNAME: first_name, + LNAME: last_name + } + } + return fetch(`https://us18.api.mailchimp.com/3.0/lists/${listId}/members`, { + method: 'POST', + headers: { + Authorization: `apikey ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) +} + +/** + * 客户端接口 + * @param {*} email + * @param {*} firstName + * @param {*} lastName + * @returns + */ +export async function subscribeToNewsletter(email, firstName, lastName) { + const response = await fetch('/api/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email, first_name: firstName, last_name: lastName }) + }) + const data = await response.json() + return data +} diff --git a/lib/notion/getNotionData.js b/lib/notion/getNotionData.js index 91aa029e..fb6bc942 100644 --- a/lib/notion/getNotionData.js +++ b/lib/notion/getNotionData.js @@ -102,7 +102,7 @@ function getCustomNav({ allPages }) { * @returns */ function getCustomMenu({ collectionData }) { - const menuPages = collectionData.filter(post => (post.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published') + const menuPages = collectionData.filter(post => (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published') const menus = [] if (menuPages && menuPages.length > 0) { menuPages.forEach(e => { @@ -287,7 +287,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { let postCount = 0 // 查找所有的Post和Page const allPages = collectionData.filter(post => { - if (post.type === 'Post' && post.status === 'Published') { + if (post?.type === 'Post' && post.status === 'Published') { postCount++ } return post && post?.slug && @@ -306,10 +306,10 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) // 旧的菜单 - const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') }) + const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') }) // 新的菜单 const customMenu = await getCustomMenu({ collectionData }) - const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 }) + const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 }) const allNavPages = getNavPages({ allPages }) return { diff --git a/lib/notion/mapImage.js b/lib/notion/mapImage.js index 0d8a7ca0..8fbfbb1c 100644 --- a/lib/notion/mapImage.js +++ b/lib/notion/mapImage.js @@ -6,7 +6,7 @@ import BLOG from '@/blog.config' * 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸 * @param {*} image */ -const compressImage = (image, width = 400) => { +const compressImage = (image, width = 400, quality = 50, fmt = 'webp') => { if (!image) { return null } @@ -20,11 +20,12 @@ const compressImage = (image, width = 400) => { // 获取URL参数 const params = new URLSearchParams(urlObj.search) // 将q参数的值替换 - params.set('q', '50') + params.set('q', quality) // 尺寸 params.set('width', width) // 格式 - params.set('fmt', 'webp') + params.set('fmt', fmt) + params.set('fm', fmt) // 生成新的URL urlObj.search = params.toString() return urlObj.toString() diff --git a/next.config.js b/next.config.js index 3569ca92..b97ad538 100644 --- a/next.config.js +++ b/next.config.js @@ -3,8 +3,31 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ }) const { THEME } = require('./blog.config') +const fs = require('fs') const path = require('path') +/** + * 扫描指定目录下的文件夹名,用于获取当前有几个主题 + * @param {*} directory + * @returns + */ +function scanSubdirectories(directory) { + const subdirectories = [] + + fs.readdirSync(directory).forEach(file => { + const fullPath = path.join(directory, file) + const stats = fs.statSync(fullPath) + + // landing主题比较特殊,不在可切换的主题中显示 + if (stats.isDirectory() && file !== 'landing') { + subdirectories.push(file) + } + }) + + return subdirectories +} +// 扫描项目 /themes下的目录名 +const themes = scanSubdirectories(path.resolve(__dirname, 'themes')) module.exports = withBundleAnalyzer({ images: { // 图片压缩 @@ -68,13 +91,15 @@ module.exports = withBundleAnalyzer({ // }) // } - // console.log(path.resolve(__dirname, 'themes', THEME)) // 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径 config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME) - return config }, experimental: { scrollRestoration: true + }, + publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到 + NODE_ENV_API: process.env.NODE_ENV_API || 'prod', + THEMES: themes } }) diff --git a/package.json b/package.json index 82c9f396..adc8d380 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notion-next", - "version": "3.16.4", + "version": "4.0.0", "homepage": "https://github.com/tangly1024/NotionNext.git", "license": "MIT", "repository": { @@ -22,6 +22,7 @@ }, "dependencies": { "@giscus/react": "^2.2.6", + "@headlessui/react": "^1.7.15", "@next/bundle-analyzer": "^12.1.1", "@vercel/analytics": "^1.0.0", "animate.css": "^4.1.1", @@ -53,7 +54,6 @@ "react-notion-x": "6.16.0", "react-share": "^4.4.1", "react-tweet-embed": "~2.0.0", - "smoothscroll-polyfill": "^0.4.4", "typed.js": "^2.0.12", "use-ackee": "^3.0.0" }, @@ -69,7 +69,7 @@ "eslint-plugin-react": "^7.23.2", "next-sitemap": "^1.6.203", "postcss": "^8.4.20", - "tailwindcss": "^3.2.4", + "tailwindcss": "^3.3.2", "webpack-bundle-analyzer": "^4.5.0" }, "resolutions": { diff --git a/pages/[...slug].js b/pages/[...slug].js index 19d073fc..1523c674 100644 --- a/pages/[...slug].js +++ b/pages/[...slug].js @@ -48,7 +48,7 @@ const Slug = props => { }) } } - }, 5 * 1000) // 404时长 8秒 + }, 8 * 1000) // 404时长 8秒 } // 文章加密 diff --git a/pages/_app.js b/pages/_app.js index 9cee751f..e0d11382 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,8 +1,9 @@ import { useEffect } from 'react' -import 'animate.css' +// import 'animate.css' import '@/styles/globals.css' import '@/styles/nprogress.css' +import '@/styles/utility-patterns.css' // core styles shared by all of react-notion-x (required) import 'react-notion-x/src/styles.css' @@ -10,11 +11,8 @@ import '@/styles/notion.css' // 重写部分样式 import { GlobalContextProvider } from '@/lib/global' -import { isMobile } from '@/lib/utils' import AOS from 'aos' import 'aos/dist/aos.css' // You can also use for styles - -import smoothscroll from 'smoothscroll-polyfill' import dynamic from 'next/dynamic' // 自定义样式css和js引入 @@ -26,9 +24,6 @@ const ExternalPlugins = dynamic(() => import('@/components/ExternalPlugins')) const MyApp = ({ Component, pageProps }) => { useEffect(() => { AOS.init() - if (isMobile()) { - smoothscroll.polyfill() - } }, []) return ( diff --git a/pages/_document.js b/pages/_document.js index c053a632..6b7daa70 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -17,7 +17,7 @@ class MyDocument extends Document { - +
diff --git a/pages/api/subscribe.js b/pages/api/subscribe.js new file mode 100644 index 00000000..86adddf9 --- /dev/null +++ b/pages/api/subscribe.js @@ -0,0 +1,22 @@ +import subscribeToMailchimpApi from '@/lib/mailchimp' + +/** + * 接受邮件订阅 + * @param {*} req + * @param {*} res + */ +export default async function handler(req, res) { + if (req.method === 'POST') { + const { email, firstName, lastName } = req.body + try { + const response = await subscribeToMailchimpApi({ email, first_name: firstName, last_name: lastName }) + const data = await response.json() + console.log('data', data) + res.status(200).json({ status: 'success', message: 'Subscription successful!' }) + } catch (error) { + res.status(400).json({ status: 'error', message: 'Subscription failed!', error }) + } + } else { + res.status(405).json({ status: 'error', message: 'Method not allowed' }) + } +} diff --git a/pages/archive/index.js b/pages/archive/index.js index 6225373c..917fee76 100644 --- a/pages/archive/index.js +++ b/pages/archive/index.js @@ -1,9 +1,10 @@ import { getGlobalData } from '@/lib/notion/getNotionData' -import React from 'react' +import { useEffect } from 'react' import { useGlobal } from '@/lib/global' import BLOG from '@/blog.config' import { useRouter } from 'next/router' import { getLayoutByTheme } from '@/themes/theme' +import { isBrowser } from '@/lib/utils' import { formatDateFmt } from '@/lib/formatDate' const ArchiveIndex = props => { @@ -13,6 +14,20 @@ const ArchiveIndex = props => { // 根据页面路径加载不同Layout文件 const Layout = getLayoutByTheme(useRouter()) + useEffect(() => { + if (isBrowser()) { + const anchor = window.location.hash + if (anchor) { + setTimeout(() => { + const anchorElement = document.getElementById(anchor.substring(1)) + if (anchorElement) { + anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' }) + } + }, 300) + } + } + }, []) + const meta = { title: `${locale.NAV.ARCHIVE} | ${siteInfo?.title}`, description: siteInfo?.description, diff --git a/pages/search/[keyword]/index.js b/pages/search/[keyword]/index.js index 59e26f96..c2e9817c 100644 --- a/pages/search/[keyword]/index.js +++ b/pages/search/[keyword]/index.js @@ -117,7 +117,7 @@ async function filterByMemCache(allPosts, keyword) { for (const post of allPosts) { const cacheKey = 'page_block_' + post.id const page = await getDataFromCache(cacheKey, true) - const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : '' + const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : '' const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' const articleInfo = post.title + post.summary + tagContent + categoryContent let hit = articleInfo.toLowerCase().indexOf(keyword) > -1 diff --git a/pages/search/[keyword]/page/[page].js b/pages/search/[keyword]/page/[page].js index e706e9b8..11a682f7 100644 --- a/pages/search/[keyword]/page/[page].js +++ b/pages/search/[keyword]/page/[page].js @@ -115,7 +115,7 @@ async function filterByMemCache(allPosts, keyword) { for (const post of allPosts) { const cacheKey = 'page_block_' + post.id const page = await getDataFromCache(cacheKey, true) - const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : '' + const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : '' const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' const articleInfo = post.title + post.summary + tagContent + categoryContent let hit = articleInfo.indexOf(keyword) > -1 diff --git a/pages/search/index.js b/pages/search/index.js index 9fe02dbb..ba09db8e 100644 --- a/pages/search/index.js +++ b/pages/search/index.js @@ -4,6 +4,11 @@ import { useRouter } from 'next/router' import BLOG from '@/blog.config' import { getLayoutByTheme } from '@/themes/theme' +/** + * 搜索路由 + * @param {*} props + * @returns + */ const Search = props => { const { posts, siteInfo } = props const { locale } = useGlobal() @@ -18,7 +23,7 @@ const Search = props => { // 静态过滤 if (keyword) { filteredPosts = posts.filter(post => { - const tagContent = post.tags ? post.tags.join(' ') : '' + const tagContent = post?.tags ? post?.tags.join(' ') : '' const categoryContent = post.category ? post.category.join(' ') : '' const searchContent = post.title + post.summary + tagContent + categoryContent diff --git a/pages/tag/[tag]/index.js b/pages/tag/[tag]/index.js index a211fc36..db2aaed8 100644 --- a/pages/tag/[tag]/index.js +++ b/pages/tag/[tag]/index.js @@ -33,7 +33,7 @@ export async function getStaticProps({ params: { tag } }) { const props = await getGlobalData({ from }) // 过滤状态 - props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag)) + props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag)) // 处理文章页数 props.postCount = props.posts.length diff --git a/pages/tag/[tag]/page/[page].js b/pages/tag/[tag]/page/[page].js index 92d01820..bf383870 100644 --- a/pages/tag/[tag]/page/[page].js +++ b/pages/tag/[tag]/page/[page].js @@ -27,7 +27,7 @@ export async function getStaticProps({ params: { tag, page } }) { const from = 'tag-page-props' const props = await getGlobalData({ from }) // 过滤状态、标签 - props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag)) + props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag)) // 处理文章数 props.postCount = props.posts.length // 处理分页 @@ -48,7 +48,7 @@ export async function getStaticPaths() { const paths = [] tagOptions?.forEach(tag => { // 过滤状态类型 - const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag.name)) + const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name)) // 处理文章页数 const postCount = tagPosts.length const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) diff --git a/public/bg_image.jpg b/public/bg_image.jpg old mode 100755 new mode 100644 index 191b9554..cb503374 Binary files a/public/bg_image.jpg and b/public/bg_image.jpg differ diff --git a/public/css/theme-fukasawa.css b/public/css/theme-fukasawa.css deleted file mode 100644 index d5a56963..00000000 --- a/public/css/theme-fukasawa.css +++ /dev/null @@ -1 +0,0 @@ -/* fukasawa的主题相关 */ \ No newline at end of file diff --git a/public/css/theme-hexo.css b/public/css/theme-hexo.css deleted file mode 100644 index ff509ec5..00000000 --- a/public/css/theme-hexo.css +++ /dev/null @@ -1,30 +0,0 @@ -/* 菜单下划线动画 */ -#theme-hexo .menu-link { - text-decoration: none; - background-image: linear-gradient(#928CEE, #928CEE); - background-repeat: no-repeat; - background-position: bottom center; - background-size: 0 2px; - transition: background-size 100ms ease-in-out; -} - -#theme-hexo .menu-link:hover { - background-size: 100% 2px; - color: #928CEE; -} - -/* 设置了从上到下的渐变黑色 */ -#theme-hexo .header-cover::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%); -} - -/* Custem */ -.tk-footer{ - opacity: 0; -} \ No newline at end of file diff --git a/public/css/theme-matery.css b/public/css/theme-matery.css deleted file mode 100644 index e3a01f01..00000000 --- a/public/css/theme-matery.css +++ /dev/null @@ -1,11 +0,0 @@ - -/* 设置了从上到下的渐变黑色 */ -#theme-matery .header-cover::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%); -} \ No newline at end of file diff --git a/public/css/theme-simple.css b/public/css/theme-simple.css deleted file mode 100644 index d02b7661..00000000 --- a/public/css/theme-simple.css +++ /dev/null @@ -1,34 +0,0 @@ -#theme-simple #announcement-content { - /* background-color: #f6f6f6; */ -} - -#theme-simple .blog-item-title { - color: #276077; -} - -.dark #theme-simple .blog-item-title { - color: #d1d5db; -} - -.notion { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - - -/* 菜单下划线动画 */ -#theme-simple .menu-link { - text-decoration: none; - background-image: linear-gradient(#dd3333, #dd3333); - background-repeat: no-repeat; - background-position: bottom center; - background-size: 0 2px; - transition: background-size 100ms ease-in-out; -} - -#theme-simple .menu-link:hover { - background-size: 100% 2px; - color: #dd3333; - cursor: pointer; -} - diff --git a/public/images/feature-1.webp b/public/images/feature-1.webp new file mode 100644 index 00000000..5ac4713b Binary files /dev/null and b/public/images/feature-1.webp differ diff --git a/public/images/feature-2.webp b/public/images/feature-2.webp new file mode 100644 index 00000000..9fa99672 Binary files /dev/null and b/public/images/feature-2.webp differ diff --git a/public/images/feature-3.webp b/public/images/feature-3.webp new file mode 100644 index 00000000..a03616cd Binary files /dev/null and b/public/images/feature-3.webp differ diff --git a/public/images/features-bg.png b/public/images/features-bg.png new file mode 100644 index 00000000..c2e77985 Binary files /dev/null and b/public/images/features-bg.png differ diff --git a/public/images/features-element.png b/public/images/features-element.png new file mode 100644 index 00000000..9f1bf79b Binary files /dev/null and b/public/images/features-element.png differ diff --git a/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp b/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp new file mode 100644 index 00000000..31fa34c8 Binary files /dev/null and b/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp differ diff --git a/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp b/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp new file mode 100644 index 00000000..0cde7cf2 Binary files /dev/null and b/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp differ diff --git a/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp b/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp new file mode 100644 index 00000000..341c67a1 Binary files /dev/null and b/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp differ diff --git a/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp b/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp new file mode 100644 index 00000000..6da52167 Binary files /dev/null and b/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp differ diff --git a/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp b/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp new file mode 100644 index 00000000..c76fccb4 Binary files /dev/null and b/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp differ diff --git a/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp b/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp new file mode 100644 index 00000000..8f6e6fa5 Binary files /dev/null and b/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp differ diff --git a/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp b/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp new file mode 100644 index 00000000..7d9a69ec Binary files /dev/null and b/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp differ diff --git a/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp b/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp new file mode 100644 index 00000000..667b0cb7 Binary files /dev/null and b/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp differ diff --git a/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp b/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp new file mode 100644 index 00000000..221d36f2 Binary files /dev/null and b/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp differ diff --git a/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp b/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp new file mode 100644 index 00000000..8a4c0240 Binary files /dev/null and b/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp differ diff --git a/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp b/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp new file mode 100644 index 00000000..53376d3a Binary files /dev/null and b/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp differ diff --git a/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp b/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp new file mode 100644 index 00000000..36f479fa Binary files /dev/null and b/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp differ diff --git a/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp b/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp new file mode 100644 index 00000000..f94947e3 Binary files /dev/null and b/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp differ diff --git a/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp b/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp new file mode 100644 index 00000000..da3e93d1 Binary files /dev/null and b/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp differ diff --git a/public/images/hero-image.png b/public/images/hero-image.png new file mode 100644 index 00000000..522befe4 Binary files /dev/null and b/public/images/hero-image.png differ diff --git a/public/images/home.png b/public/images/home.png new file mode 100644 index 00000000..cb154505 Binary files /dev/null and b/public/images/home.png differ diff --git a/public/images/testimonial.jpg b/public/images/testimonial.jpg new file mode 100644 index 00000000..8eadf88e Binary files /dev/null and b/public/images/testimonial.jpg differ diff --git a/public/videos/video.mp4 b/public/videos/video.mp4 new file mode 100644 index 00000000..92686932 Binary files /dev/null and b/public/videos/video.mp4 differ diff --git a/styles/globals.css b/styles/globals.css index 042c7f28..dbaa8ef2 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2,28 +2,6 @@ @tailwind components; @tailwind utilities; -html { - --scrollbarBG: #ffffff00; - --thumbBG: #b8b8b8; -} -body::-webkit-scrollbar { - width: 5px; -} -body { - scrollbar-width: thin; - scrollbar-color: var(--thumbBG) var(--scrollbarBG); -} -body::-webkit-scrollbar-track { - background: var(--scrollbarBG); -} -body::-webkit-scrollbar-thumb { - background-color: var(--thumbBG); -} - -::selection { - background: rgba(45, 170, 219, 0.3); -} - .wrapper { min-height: 100vh; display: flex; @@ -285,66 +263,3 @@ a.avatar-wrapper { .reply-author-name { font-weight: 500; } - -.p-4-lines { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 4; - overflow: hidden; - text-overflow: ellipsis; -} - -.p-3-lines { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - overflow: hidden; - text-overflow: ellipsis; -} - -/* fukasawa的首页响应式分栏 */ -#theme-fukasawa .grid-item { - height: auto; - break-inside: avoid-column; - margin-bottom: .5rem; - } - - /* 大屏幕(宽度≥1024px)下显示3列 */ - @media (min-width: 1024px) { - #theme-fukasawa .grid-container { - column-count: 3; - column-gap: .5rem; - } - } - - /* 小屏幕(宽度≥640px)下显示2列 */ - @media (min-width: 640px) and (max-width: 1023px) { - #theme-fukasawa .grid-container { - column-count: 2; - column-gap: .5rem; - } - } - - /* 移动端(宽度<640px)下显示1列 */ - @media (max-width: 639px) { - #theme-fukasawa .grid-container { - column-count: 1; - column-gap: .5rem; - } - } - -.notion-external-title { - @apply dark:text-white !important; -} - -.notion-external-subtitle { - @apply dark:text-gray-400 !important; -} - -.notion-external-block { - @apply dark:border-gray-200 !important; -} - -.notion-external-image > svg > g > path{ - @apply dark:fill-gray-200 !important; -} \ No newline at end of file diff --git a/styles/notion.css b/styles/notion.css index b943aff9..b3bde97e 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -179,9 +179,6 @@ color: var(--select-color-2) !important; } -.notion-simple-table { - @apply whitespace-nowrap overflow-x-auto block -} .notion-app { position: relative; @@ -446,6 +443,7 @@ summary > .notion-h { .notion-h:hover .notion-hash-link { opacity: 1; + @apply dark:fill-gray-200 } .notion-hash-link { @@ -1939,25 +1937,13 @@ svg + .notion-page-title-text { } .notion-simple-table { - width: 100% !important; + @apply whitespace-nowrap overflow-x-auto block w-full border-0 !important; } .notion-asset-wrapper-pdf > div { display: block !important; } -::selection { - @apply bg-blue-500 text-gray-50 !important; -} - -.dark img{ - @apply opacity-80 -} - -.dark #live2d { - @apply opacity-80 -} - /* https://github.com/kchen0x */ .notion-quote { display: block; @@ -2026,3 +2012,38 @@ code.language-mermaid { .notion-equation-inline .katex-display { margin: 0 0 !important; } + +.notion-external-title { + @apply dark:text-white !important; +} + +.notion-external-subtitle { + @apply dark:text-gray-400 !important; +} + +.notion-external-block { + @apply dark:border-gray-200 !important; +} + +.notion-external-image > svg > g > path{ + @apply dark:fill-gray-200 !important; +} + +.notion-external-image { + @apply w-6 h-6 mx-3 my-2 !important; +} + +/* 表格 #f5f6f8*/ +.notion-simple-table-row { +} + +/* 表格头 */ +.notion-simple-table tr:first-child td{ + background-color: #f5f6f8; + @apply text-center font-bold dark:bg-gray-800 !important; + +} + +.notion-simple-table td{ + border: 1px solid var(#eee) !important +} \ No newline at end of file diff --git a/styles/utility-patterns.css b/styles/utility-patterns.css new file mode 100644 index 00000000..383a3384 --- /dev/null +++ b/styles/utility-patterns.css @@ -0,0 +1,79 @@ +/* Typography */ +.h1 { + @apply text-4xl font-extrabold leading-tight tracking-tighter; +} + +.h2 { + @apply text-3xl font-extrabold leading-tight tracking-tighter; +} + +.h3 { + @apply text-3xl font-bold leading-tight; +} + +.h4 { + @apply text-2xl font-bold leading-snug tracking-tight; +} + +@screen md { + .h1 { + @apply text-5xl; + } + + .h2 { + @apply text-4xl; + } +} + +/* Buttons */ +.btn, +.btn-sm { + @apply font-medium inline-flex items-center justify-center border border-transparent rounded leading-snug transition duration-150 ease-in-out; +} + +.btn { + @apply px-8 py-3 shadow-lg; +} + +.btn-sm { + @apply px-4 py-2 shadow; +} + +/* Forms */ +.form-input, +.form-textarea, +.form-multiselect, +.form-select, +.form-checkbox, +.form-radio { + @apply bg-white border border-gray-300 focus:border-gray-500; +} + +.form-input, +.form-textarea, +.form-multiselect, +.form-select, +.form-checkbox { + @apply rounded; +} + +.form-input, +.form-textarea, +.form-multiselect, +.form-select { + @apply py-3 px-4; +} + +.form-input, +.form-textarea { + @apply placeholder-gray-500; +} + +.form-select { + @apply pr-10; +} + +.form-checkbox, +.form-radio { + @apply text-gray-800 rounded-sm; +} \ No newline at end of file diff --git a/themes/example/Layout404.js b/themes/example/Layout404.js deleted file mode 100644 index c0992ad3..00000000 --- a/themes/example/Layout404.js +++ /dev/null @@ -1,9 +0,0 @@ -import LayoutBase from './LayoutBase' - -export const Layout404 = (props) => { - return - 404 Not found. - -} - -export default Layout404 diff --git a/themes/example/LayoutArchive.js b/themes/example/LayoutArchive.js deleted file mode 100644 index e36a3210..00000000 --- a/themes/example/LayoutArchive.js +++ /dev/null @@ -1,47 +0,0 @@ -import BLOG from '@/blog.config' -import Link from 'next/link' -import LayoutBase from './LayoutBase' - -export const LayoutArchive = props => { - const { archivePosts } = props - - return ( - -
- {Object.keys(archivePosts).map(archiveTitle => ( -
-
- {archiveTitle} -
- -
    - {archivePosts[archiveTitle].map(post => ( -
  • -
    - - {post.date?.start_date} - {' '} -   - - - {post.title} - - -
    -
  • - ))} -
-
- ))} -
-
- ) -} - -export default LayoutArchive diff --git a/themes/example/LayoutBase.js b/themes/example/LayoutBase.js deleted file mode 100644 index 9ca0eb5e..00000000 --- a/themes/example/LayoutBase.js +++ /dev/null @@ -1,60 +0,0 @@ -import CommonHead from '@/components/CommonHead' -import React from 'react' -import { Header } from './components/Header' -import { Nav } from './components/Nav' -import { Footer } from './components/Footer' -import { Title } from './components/Title' -import { SideBar } from './components/SideBar' -import JumpToTopButton from './components/JumpToTopButton' -import BLOG from '@/blog.config' -import { useGlobal } from '@/lib/global' -/** - * 基础布局 采用左右两侧布局,移动端使用顶部导航栏 - - * @returns {JSX.Element} - * @constructor - */ -const LayoutBase = props => { - const { children, meta } = props - const { onLoading } = useGlobal() - - const LoadingCover =
-
- -
-
- - return ( -
- - {/* 顶栏LOGO */} -
- - {/* 菜单 */} -