This commit is contained in:
tangly1024
2021-09-27 09:33:21 +08:00
parent 22ca7f6d63
commit dfc0f645d4
76 changed files with 3650 additions and 2 deletions

21
lib/cache/cache_manager.js vendored Normal file
View File

@@ -0,0 +1,21 @@
import { getCacheFromFile, setCacheToFile } from '@/lib/cache/local_file_cache'
import { getCacheFromMemory, setCacheToMemory } from '@/lib/cache/memory_cache'
import BLOG from '@/blog.config'
export async function getDataFromCache (key) {
let dataFromCache
if (BLOG.isProd) {
dataFromCache = await getCacheFromMemory(key)
} else {
dataFromCache = await getCacheFromFile(key)
}
return dataFromCache
}
export async function setDataToCache (key, data) {
if (BLOG.isProd) {
await setCacheToMemory(key, data)
} else {
await setCacheToFile(key, data)
}
}

36
lib/cache/local_file_cache.js vendored Normal file
View File

@@ -0,0 +1,36 @@
import fs from 'fs'
import BLOG from '@/blog.config'
const path = require('path')
// 文件缓存持续10秒
const cacheInvalidSeconds = 1000000000 * 1000
// 文件名
const jsonFile = path.resolve('./data.json')
export async function getCacheFromFile (key) {
const exist = await fs.existsSync(jsonFile)
if (!exist) return null
const data = await fs.readFileSync(jsonFile)
const json = data ? JSON.parse(data) : {}
// 缓存超过有效期就作废
const cacheValidTime = new Date(parseInt(json[key + '_expire_time']) + cacheInvalidSeconds)
const currentTime = new Date()
if (!cacheValidTime || cacheValidTime < currentTime) {
return null
}
return json[key]
}
/**
* 并发请求写文件异常; Vercel生产环境不支持写文件。
* @param key
* @param data
* @returns {Promise<null>}
*/
export async function setCacheToFile (key, data) {
const exist = await fs.existsSync(jsonFile)
const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
json[key] = data
json[key + '_expire_time'] = new Date().getTime()
fs.writeFileSync(jsonFile, JSON.stringify(json))
}

9
lib/cache/memory_cache.js vendored Normal file
View File

@@ -0,0 +1,9 @@
import cache from 'memory-cache'
export async function getCacheFromMemory (key, options) { // url为缓存标识
return cache.get(key)
}
export async function setCacheToMemory (key, data) { // url为缓存标识
await cache.put(key, data, 60 * 1000)
}

21
lib/cjk.js Normal file
View File

@@ -0,0 +1,21 @@
const BLOG = require('../blog.config')
module.exports = function () {
switch (BLOG.lang.toLowerCase()) {
case 'zh-cn':
case 'zh-sg':
return 'SC'
case 'zh':
case 'zh-hk':
case 'zh-tw':
return 'TC'
case 'ja':
case 'ja-jp':
return 'JP'
case 'ko':
case 'ko-kr':
return 'KR'
default:
return null
}
}

8
lib/formatDate.js Normal file
View File

@@ -0,0 +1,8 @@
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'
? res.replace('年', ' 年 ').replace('月', ' 月 ').replace('日', ' 日')
: res
}

18
lib/gtag.js Normal file
View File

@@ -0,0 +1,18 @@
import BLOG from '@/blog.config'
export const GA_TRACKING_ID = BLOG.analytics.gaConfig.measurementId
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = url => {
window.gtag('config', GA_TRACKING_ID, {
page_path: url
})
}
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }) => {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value
})
}

83
lib/lang.js Normal file
View File

@@ -0,0 +1,83 @@
const lang = {
en: {
NAV: {
INDEX: 'Blog',
RSS: 'RSS',
SEARCH: 'Search',
ABOUT: 'About',
NAVGATION: 'NAVGATION'
},
PAGINATION: {
PREV: 'Prev',
NEXT: 'Next'
},
SEARCH: {
ARTICLES: 'Search Articles',
TAGS: 'Search in'
},
POST: {
BACK: 'Back',
TOP: 'Top'
}
},
'zh-CN': {
NAV: {
INDEX: '首页',
RSS: '订阅',
SEARCH: '搜索',
ABOUT: '关于',
NAVGATION: '导航'
},
PAGINATION: {
PREV: '上一页',
NEXT: '下一页'
},
SEARCH: {
ARTICLES: '搜索文章',
TAGS: '搜索标签'
},
POST: {
BACK: '返回上页',
TOP: '回到顶部'
}
},
'zh-HK': {
NAV: {
INDEX: '網誌',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
NAVGATION: '導航'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
SEARCH: {
ARTICLES: '搜尋文章',
TAGS: '搜尋標簽'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
},
'zh-TW': {
NAV: {
INDEX: '部落格',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
NAVGATION: '導航'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
}
}
export default lang

34
lib/locale.js Normal file
View File

@@ -0,0 +1,34 @@
import BLOG from '@/blog.config'
import lang from './lang'
import { useContext, createContext } from 'react'
let locale = {}
if (BLOG.lang.slice(0, 2).toLowerCase() === 'zh') {
switch (BLOG.lang.toLowerCase()) {
case 'zh-cn':
case 'zh-sg':
locale = lang['zh-CN']
break
case 'zh-hk':
locale = lang['zh-HK']
break
case 'zh-tw':
locale = lang['zh-TW']
break
default:
locale = lang['zh-TW']
break
}
} else {
locale = lang.en
}
const LocaleContext = createContext()
export function LocaleProvider({ children }) {
return (
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
)
}
export const useLocale = () => useContext(LocaleContext)

3
lib/notion.js Normal file
View File

@@ -0,0 +1,3 @@
export { getAllPosts } from './notion/getAllPosts'
export { getAllTags } from './notion/getAllTags'
export { getPostBlocks } from './notion/getPostBlocks'

View File

@@ -0,0 +1,20 @@
import { idToUuid } from 'notion-utils'
export default function getAllPageIds (collectionQuery, viewId) {
const views = Object.values(collectionQuery)[0]
if (!views) {
return []
}
let pageIds = []
if (viewId) {
const vId = idToUuid(viewId)
pageIds = views[vId]?.blockIds
} else {
const pageSet = new Set()
Object.values(views).forEach(view => {
view?.blockIds?.forEach(id => pageSet.add(id))
})
pageIds = [...pageSet]
}
return pageIds
}

101
lib/notion/getAllPosts.js Normal file
View File

@@ -0,0 +1,101 @@
import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils'
import getAllPageIds from './getAllPageIds'
import getPageProperties from './getPageProperties'
import { defaultMapImageUrl } from 'react-notion-x'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getPostBlocks } from '@/lib/notion/getPostBlocks'
export async function getAllPosts () {
const data = await getDataFromCache('posts_list')
if (data) {
return data
}
let id = BLOG.notionPageId
const pageRecordMap = await getPostBlocks(id)
if (!pageRecordMap) {
return <>获取数据异常</>
}
id = idToUuid(id)
const collection = Object.values(pageRecordMap.collection)[0]?.value
const collectionQuery = pageRecordMap.collection_query
const block = pageRecordMap.block
const schema = collection?.schema
const rawMetadata = block[id].value
// Check Type 兼容Page-Database和Inline-Database
if (rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view') {
console.warn(`pageId "${id}" is not a database`)
return null
} else {
// Construct Data
const pageIds = getAllPageIds(collectionQuery)
const data = []
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const properties = (await getPageProperties(id, block, schema)) || null
// Add fullwidth, createdtime to properties
properties.createdTime = new Date(
block[id].value?.created_time
).toString()
properties.fullWidth = block[id].value?.format?.page_full_width ?? false
properties.page_cover = getPostCover(id, block, pageRecordMap) ?? getContentFirstImage(id, block, pageRecordMap)
properties.content = block[id].value?.content ?? []
data.push(properties)
}
// remove all the the items doesn't meet requirements
const posts = data.filter(post => {
return (
post.title &&
post.slug &&
post?.status?.[0] === 'Published' &&
(post?.type?.[0] === 'Post' || post?.type?.[0] === 'Page')
)
})
// Sort by date
if (BLOG.sortByDate) {
posts.sort((a, b) => {
const dateA = new Date(a?.date?.start_date || a.createdTime)
const dateB = new Date(b?.date?.start_date || b.createdTime)
return dateB - dateA
})
}
if (posts) {
await setDataToCache('posts_list', posts)
}
return posts
}
}
// 从Block获取封面图;优先取PageCover否则取内容图片
function getPostCover (id, block, pageRecordMap) {
const pageCover = block[id].value?.format?.page_cover
if (pageCover) {
if (pageCover.startsWith('/')) return 'https://www.notion.so' + pageCover
if (pageCover.startsWith('http')) return defaultMapImageUrl(pageCover, block[id].value)
}
}
// 取文章的第一个图片内容作为封面
function getContentFirstImage (id, block, pageRecordMap) {
const pageBlock = block[id]?.value
const contentBlockId = pageBlock?.content?.find((blockId) => {
const block = pageRecordMap.block[blockId]?.value
if (block?.type === 'image') {
return true
}
})
if (contentBlockId) {
const contentBlock = pageRecordMap.block[contentBlockId]?.value
const source = contentBlock.properties?.source?.[0]?.[0] ??
contentBlock.format?.display_source
return defaultMapImageUrl(source, contentBlock)
}
return ''
}

23
lib/notion/getAllTags.js Normal file
View File

@@ -0,0 +1,23 @@
import { getAllPosts } from './getAllPosts'
export async function getAllTags (posts) {
if (!posts) {
const response = await getAllPosts()
posts = response.filter(
post =>
post.status[0] === 'Published' && post.type[0] === 'Post' && post.tags
)
}
let tags = posts.map(p => p.tags)
tags = [...tags.flat()]
const tagObj = {}
tags.forEach(tag => {
if (tag in tagObj) {
tagObj[tag]++
} else {
tagObj[tag] = 1
}
})
return tagObj
}

11
lib/notion/getMetadata.js Normal file
View File

@@ -0,0 +1,11 @@
export default function getMetadata(rawMetadata) {
const metadata = {
locked: rawMetadata?.format?.block_locked,
page_full_width: rawMetadata?.format?.page_full_width,
page_font: rawMetadata?.format?.page_font,
page_small_text: rawMetadata?.format?.page_small_text,
created_time: rawMetadata.created_time,
last_edited_time: rawMetadata.last_edited_time
}
return metadata
}

View File

@@ -0,0 +1,60 @@
import { getTextContent, getDateValue } from 'notion-utils'
import { NotionAPI } from 'notion-client'
async function getPageProperties (id, block, schema, authToken) {
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
const excludeProperties = ['date', 'select', 'multi_select', 'person']
const properties = {}
for (let i = 0; i < rawProperties.length; i++) {
const [key, val] = rawProperties[i]
properties.id = id
if (schema[key]?.type && !excludeProperties.includes(schema[key].type)) {
properties[schema[key].name] = getTextContent(val)
} else {
switch (schema[key]?.type) {
case 'date': {
const dateProperty = getDateValue(val)
delete dateProperty.type
properties[schema[key].name] = dateProperty
break
}
case 'select':
case 'multi_select': {
const selects = getTextContent(val)
if (selects[0]?.length) {
properties[schema[key].name] = selects.split(',')
}
break
}
case 'person': {
const rawUsers = val.flat()
const users = []
const api = new NotionAPI({ authToken })
for (let i = 0; i < rawUsers.length; i++) {
if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0]
const res = await api.getUsers(userId)
const resValue =
res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = {
id: resValue?.id,
first_name: resValue?.given_name,
last_name: resValue?.family_name,
profile_photo: resValue?.profile_photo
}
users.push(user)
}
}
properties[schema[key].name] = users
break
}
default:
break
}
}
}
return properties
}
export { getPageProperties as default }

View File

@@ -0,0 +1,69 @@
const indentLevels = {
header: 0,
sub_header: 1,
sub_sub_header: 2
}
export const getPageTableOfContents = (page,recordMap)=> {
// 获取 header sub_header sub_sub_header
const toc = (page.content ?? [])
.map((blockId) => {
const block = recordMap.block[blockId]?.value
if (block) {
const { type } = block
if (
type === 'header' ||
type === 'sub_header' ||
type === 'sub_sub_header'
) {
return {
id: blockId,
type,
indentLevel: indentLevels[type]
}
}
}
return null
})
.filter(Boolean)
const indentLevelStack = [
{
actual: -1,
effective: -1
}
]
// Adjust indent levels to always change smoothly.
// This is a little tricky, but the key is that when increasing indent levels,
// they should never jump more than one at a time.
for (const tocItem of toc) {
const { indentLevel } = tocItem
const actual = indentLevel
do {
const prevIndent = indentLevelStack[indentLevelStack.length - 1]
const { actual: prevActual, effective: prevEffective } = prevIndent
if (actual > prevActual) {
tocItem.indentLevel = prevEffective + 1
indentLevelStack.push({
actual,
effective: tocItem.indentLevel
})
} else if (actual === prevActual) {
tocItem.indentLevel = prevEffective
break
} else {
indentLevelStack.pop()
}
} while (true)
}
return toc
}

View File

@@ -0,0 +1,17 @@
import BLOG from '@/blog.config'
import { NotionAPI } from 'notion-client'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
export async function getPostBlocks (id) {
let pageBlock = await getDataFromCache('page_block_' + id)
if (pageBlock) {
return pageBlock
}
const authToken = BLOG.notionAccessToken || null
const api = new NotionAPI({ authToken })
pageBlock = await api.getPage(id)
if (pageBlock) {
await setDataToCache('page_block_' + id, pageBlock)
}
return pageBlock
}

30
lib/rss.js Normal file
View File

@@ -0,0 +1,30 @@
import { Feed } from 'feed'
import BLOG from '@/blog.config'
export function generateRss(posts) {
const year = new Date().getFullYear()
const feed = new Feed({
title: BLOG.title,
description: BLOG.description,
id: `${BLOG.link}/${BLOG.path}`,
link: `${BLOG.link}/${BLOG.path}`,
language: BLOG.lang,
favicon: `${BLOG.link}/favicon.png`,
copyright: `All rights reserved ${year}, ${BLOG.author}`,
author: {
name: BLOG.author,
email: BLOG.email,
link: BLOG.link
}
})
posts.forEach(post => {
feed.addItem({
title: post.title,
id: `${BLOG.link}/${post.slug}`,
link: `${BLOG.link}/${post.slug}`,
description: post.summary,
date: new Date(post?.date?.start_date || post.createdTime)
})
})
return feed.rss2()
}

19
lib/theme.js Normal file
View File

@@ -0,0 +1,19 @@
import { useContext, createContext, useState, useEffect } from 'react'
import localStorage from 'localStorage'
const ThemeContext = createContext()
export function ThemeProvider ({ children }) {
// 初始值
const defaultTheme = localStorage.getItem('theme')
const [theme, changeTheme] = useState()
useEffect(() => {
changeTheme(defaultTheme)
})
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>{children}</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)