Add GitBook theme structure with Traditional Chinese docs

This commit is contained in:
2025-09-17 13:06:47 +08:00
parent 6f6671a024
commit 49dab41935
44 changed files with 2675 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
'use client'
import BlogArchiveItem from '../components/BlogArchiveItem'
/**
* 歸檔頁面
* 主要依靠頁面導覽
*/
const LayoutArchive = props => {
const { archivePosts } = props
return (
<>
<div className='mb-10 pb-20 md:py-12 py-3 min-h-full'>
{Object.keys(archivePosts)?.map(archiveTitle => (
<BlogArchiveItem
key={archiveTitle}
archiveTitle={archiveTitle}
archivePosts={archivePosts}
/>
))}
</div>
</>
)
}
export { LayoutArchive }

View File

@@ -0,0 +1,54 @@
'use client'
import NotionPage from '@/components/NotionPage'
import { SignIn, SignUp } from '@clerk/nextjs'
/**
* 登入頁面
*/
const LayoutSignIn = props => {
const { post } = props
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
return (
<>
<div className='grow mt-20'>
{/* Clerk 預設表單 */}
{enableClerk && (
<div className='flex justify-center py-6'>
<SignIn />
</div>
)}
<div id='article-wrapper'>
<NotionPage post={post} />
</div>
</div>
</>
)
}
/**
* 註冊頁面
*/
const LayoutSignUp = props => {
const { post } = props
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
return (
<>
<div className='grow mt-20'>
{/* Clerk 預設表單 */}
{enableClerk && (
<div className='flex justify-center py-6'>
<SignUp />
</div>
)}
<div id='article-wrapper'>
<NotionPage post={post} />
</div>
</div>
</>
)
}
export { LayoutSignIn, LayoutSignUp }

View File

@@ -0,0 +1,223 @@
'use client'
import { AdSlot } from '@/components/GoogleAdsense'
import Live2D from '@/components/Live2D'
import LoadingCover from '@/components/LoadingCover'
import dynamic from 'next/dynamic'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import { getShortId } from '@/lib/utils/pageId'
import Announcement from '../components/Announcement'
import ArticleInfo from '../components/ArticleInfo'
import BottomMenuBar from '../components/BottomMenuBar'
import Catalog from '../components/Catalog'
import Footer from '../components/Footer'
import Header from '../components/Header'
import InfoCard from '../components/InfoCard'
import JumpToTopButton from '../components/JumpToTopButton'
import NavPostList from '../components/NavPostList'
import PageNavDrawer from '../components/PageNavDrawer'
import RevolverMaps from '../components/RevolverMaps'
import CONFIG from '../config'
import { Style } from '../style'
const AlgoliaSearchModal = dynamic(
() => import('@/components/AlgoliaSearchModal'),
{ ssr: false }
)
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
// 主題全域變數
const ThemeGlobalGitbook = createContext()
const useGitBookGlobal = () => useContext(ThemeGlobalGitbook)
/**
* 為最新文章加入紅點標記
*/
function getNavPagesWithLatest(allNavPages, latestPosts, post) {
// localStorage 儲存 id 與上次閱讀時間戳: posts_read_time = {"${post.id}":"Date()"}
const postReadTime = JSON.parse(
localStorage.getItem('post_read_time') || '{}'
)
if (post) {
postReadTime[getShortId(post.id)] = new Date().getTime()
}
// 更新記錄
localStorage.setItem('post_read_time', JSON.stringify(postReadTime))
return allNavPages?.map(item => {
const res = {
short_id: item.short_id,
title: item.title || '',
pageCoverThumbnail: item.pageCoverThumbnail || '',
category: item.category || null,
tags: item.tags || null,
summary: item.summary || null,
slug: item.slug,
href: item.href,
pageIcon: item.pageIcon || '',
lastEditedDate: item.lastEditedDate
}
// 屬於最新的文章通常 6 篇 &&(無閱讀紀錄 || 最近更新時間大於上次閱讀時間)
if (
latestPosts.some(post => post?.id.indexOf(item?.short_id) === 14) &&
(!postReadTime[item.short_id] ||
postReadTime[item.short_id] < new Date(item.lastEditedDate).getTime())
) {
return { ...res, isLatest: true }
} else {
return res
}
})
}
/**
* 基礎佈局
* 左右雙欄,手機版改為頂部導覽列
*/
const LayoutBase = props => {
const {
children,
post,
allNavPages,
latestPosts,
slotLeft,
slotRight,
slotTop
} = props
const { fullWidth } = useGlobal()
const router = useRouter()
const [tocVisible, changeTocVisible] = useState(false)
const [pageNavVisible, changePageNavVisible] = useState(false)
const [filteredNavPages, setFilteredNavPages] = useState(allNavPages)
const searchModal = useRef(null)
useEffect(() => {
setFilteredNavPages(getNavPagesWithLatest(allNavPages, latestPosts, post))
}, [router])
const GITBOOK_LOADING_COVER = siteConfig(
'GITBOOK_LOADING_COVER',
true,
CONFIG
)
return (
<ThemeGlobalGitbook.Provider
value={{
searchModal,
tocVisible,
changeTocVisible,
filteredNavPages,
setFilteredNavPages,
allNavPages,
pageNavVisible,
changePageNavVisible
}}>
<Style />
<div
id='theme-gitbook'
className={`${siteConfig('FONT_STYLE')} pb-16 md:pb-0 scroll-smooth bg-white dark:bg-black w-full h-full min-h-screen justify-center dark:text-gray-300`}>
<AlgoliaSearchModal cRef={searchModal} {...props} />
{/* 頂部導覽列 */}
<Header {...props} />
<main
id='wrapper'
className={`${siteConfig('LAYOUT_SIDEBAR_REVERSE') ? 'flex-row-reverse' : ''} relative flex justify-between w-full gap-x-6 h-full mx-auto max-w-screen-4xl`}>
{/* 左側抽屜 */}
{fullWidth ? null : (
<div className={'hidden md:block relative z-10 '}>
<div className='w-80 pt-14 pb-4 sticky top-0 h-screen flex justify-between flex-col'>
{/* 導覽清單 */}
<div className='overflow-y-scroll scroll-hidden pt-10 pl-5'>
{/* 嵌入區塊 */}
{slotLeft}
{/* 文章列表 */}
<NavPostList filteredNavPages={filteredNavPages} {...props} />
</div>
{/* 頁尾 */}
<Footer {...props} />
</div>
</div>
)}
{/* 內容區域 */}
<div
id='center-wrapper'
className='flex flex-col justify-between w-full relative z-10 pt-14 min-h-screen'>
<div
id='container-inner'
className={`w-full ${fullWidth ? 'px-5' : 'max-w-3xl px-3 lg:px-0'} justify-center mx-auto`}>
{slotTop}
<WWAds className='w-full' orientation='horizontal' />
{children}
{/* Google 廣告 */}
<AdSlot type='in-article' />
<WWAds className='w-full' orientation='horizontal' />
</div>
{/* 手機版頁尾 */}
<div className='md:hidden'>
<Footer {...props} />
</div>
</div>
{/* 右側資訊欄 */}
{fullWidth ? null : (
<div
className={
'w-72 hidden 2xl:block dark:border-transparent flex-shrink-0 relative z-10 '
}>
<div className='py-14 sticky top-0'>
<ArticleInfo post={props?.post ? props?.post : props.notice} />
<div>
{/* 桌面版目錄 */}
<Catalog {...props} />
{slotRight}
{router.route === '/' && (
<>
<InfoCard {...props} />
{siteConfig(
'GITBOOK_WIDGET_REVOLVER_MAPS',
null,
CONFIG
) === 'true' && <RevolverMaps />}
<Live2D />
</>
)}
{/* 主題首頁僅顯示公告 */}
<Announcement {...props} />
</div>
<AdSlot type='in-article' />
<Live2D />
</div>
</div>
)}
</main>
{GITBOOK_LOADING_COVER && <LoadingCover />}
{/* 回頂按鈕 */}
<JumpToTopButton />
{/* 手機版導覽抽屜 */}
<PageNavDrawer {...props} filteredNavPages={filteredNavPages} />
{/* 手機版底部導覽列 */}
<BottomMenuBar {...props} />
</div>
</ThemeGlobalGitbook.Provider>
)
}
export { LayoutBase, useGitBookGlobal }

View File

@@ -0,0 +1,101 @@
'use client'
import NotionPage from '@/components/NotionPage'
import { siteConfig } from '@/lib/config'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import DashboardBody from '../components/ui/dashboard/DashboardBody'
import DashboardHeader from '../components/ui/dashboard/DashboardHeader'
import CONFIG from '../config'
/**
* 首頁
* 重新導向到指定的文章詳情頁
*/
const LayoutIndex = props => {
const router = useRouter()
const index = siteConfig('GITBOOK_INDEX_PAGE', 'about', CONFIG)
const [hasRedirected, setHasRedirected] = useState(false) // 用來追蹤是否已重新導向
useEffect(() => {
const tryRedirect = async () => {
if (!hasRedirected) {
// 僅在尚未導向時執行
setHasRedirected(true)
// 重新導向至指定文章
await router.push(index)
// 使用 setTimeout 檢查頁面載入狀態
setTimeout(() => {
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
console.log('請檢查您的 Notion 資料庫中是否包含此 slug 頁面: ', index)
// 顯示錯誤訊息
const containerInner = document.querySelector(
'#theme-gitbook #container-inner'
)
const newHTML = `<h1 class="text-3xl pt-12 dark:text-gray-300">設定錯誤</h1><blockquote class="notion-quote notion-block-ce76391f3f2842d386468ff1eb705b92"><div>請在您的 Notion 中新增一篇 slug 為 ${index} 的文章</div></blockquote>`
containerInner?.insertAdjacentHTML('afterbegin', newHTML)
}
}, 2000)
}
}
if (index) {
console.log('重新導向', index)
tryRedirect()
} else {
console.log('未重新導向', index)
}
}, [index, hasRedirected, router])
return null
}
/**
* 文章列表
* 主要依靠頁面導覽
*/
const LayoutPostList = () => {
return <></>
}
/**
* 文章搜尋頁
* 主要依靠頁面導覽
*/
const LayoutSearch = () => {
return <></>
}
/**
* 儀表板
*/
const LayoutDashboard = props => {
const { post } = props
return (
<>
<div className='container grow'>
<div className='flex flex-wrap justify-center -mx-4'>
<div id='container-inner' className='w-full p-4'>
{post && (
<div id='article-wrapper' className='mx-auto'>
<NotionPage {...props} />
</div>
)}
</div>
</div>
</div>
{/* 儀表板 */}
<DashboardHeader />
<DashboardBody />
</>
)
}
export { LayoutDashboard, LayoutIndex, LayoutPostList, LayoutSearch }

View File

@@ -0,0 +1,43 @@
'use client'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import { useEffect } from 'react'
import { useRouter } from 'next/router'
/**
* 404 頁面
*/
const Layout404 = () => {
const router = useRouter()
const { locale } = useGlobal()
useEffect(() => {
// 延遲 3 秒若載入失敗就返回首頁
setTimeout(() => {
const article = isBrowser && document.getElementById('article-wrapper')
if (!article) {
router.push('/').then(() => {
// console.log('找不到頁面', router.asPath)
})
}
}, 3000)
}, [router])
return (
<>
<div className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
<div className='dark:text-gray-200'>
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'>
<i className='mr-2 fas fa-spinner animate-spin' />
404
</h2>
<div className='inline-block text-left h-32 leading-10 items-center'>
<h2 className='m-0 p-0'>{locale.NAV.PAGE_NOT_FOUND_REDIRECT}</h2>
</div>
</div>
</div>
</>
)
}
export { Layout404 }

View File

@@ -0,0 +1,111 @@
'use client'
import Comment from '@/components/Comment'
import NotionIcon from '@/components/NotionIcon'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config'
import { isBrowser } from '@/lib/utils'
import Head from 'next/head'
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import CategoryItem from '../components/CategoryItem'
import { ArticleLock } from '../components/ArticleLock'
import ArticleAround from '../components/ArticleAround'
import CatalogDrawerWrapper from '../components/CatalogDrawerWrapper'
import TagItemMini from '../components/TagItemMini'
import CONFIG from '../config'
/**
* 文章詳情頁
*/
const LayoutSlug = props => {
const { post, prev, next, siteInfo, lock, validPassword } = props
const router = useRouter()
// 若為文件首頁文章則更新瀏覽器標題
const index = siteConfig('GITBOOK_INDEX_PAGE', 'about', CONFIG)
const basePath = router.asPath.split('?')[0]
const title =
basePath?.indexOf(index) > 0
? `${post?.title} | ${siteInfo?.description}`
: `${post?.title} | ${siteInfo?.title}`
const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
useEffect(() => {
// 404 處理
if (!post) {
setTimeout(() => {
if (isBrowser) {
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
router.push('/404').then(() => {
console.warn('找不到頁面', router.asPath)
})
}
}
}, waiting404)
}
}, [post, router, waiting404])
return (
<>
<Head>
<title>{title}</title>
</Head>
{/* 文章加鎖 */}
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && (
<div id='container'>
{/* 標題 */}
<h1 className='text-3xl pt-12 dark:text-gray-300'>
{siteConfig('POST_TITLE_ICON') && (
<NotionIcon icon={post?.pageIcon} />
)}
{post?.title}
</h1>
{/* Notion 文章主體 */}
{post && (
<section className='px-1'>
<div id='article-wrapper'>
<NotionPage post={post} />
</div>
{/* 分享 */}
<ShareBar post={post} />
{/* 文章分類與標籤資訊 */}
<div className='flex justify-between'>
{siteConfig('POST_DETAIL_CATEGORY') && post?.category && (
<CategoryItem category={post.category} />
)}
<div>
{siteConfig('POST_DETAIL_TAG') &&
post?.tagItems?.map(tag => (
<TagItemMini key={tag.name} tag={tag} />
))}
</div>
</div>
{post?.type === 'Post' && (
<ArticleAround prev={prev} next={next} />
)}
{/* <AdSlot />
<WWAds className='w-full' orientation='horizontal' /> */}
<Comment frontMatter={post} />
</section>
)}
{/* 文章目錄 */}
<CatalogDrawerWrapper {...props} />
</div>
)}
</>
)
}
export { LayoutSlug }

View File

@@ -0,0 +1,72 @@
'use client'
import SmartLink from '@/components/SmartLink'
import { useGlobal } from '@/lib/global'
import TagItemMini from '../components/TagItemMini'
/**
* 分類列表
*/
const LayoutCategoryIndex = props => {
const { categoryOptions } = props
const { locale } = useGlobal()
return (
<>
<div className='bg-white dark:bg-gray-700 py-10'>
<div className='dark:text-gray-200 mb-5'>
<i className='mr-4 fas fa-th' />
{locale.COMMON.CATEGORY}:
</div>
<div id='category-list' className='duration-200 flex flex-wrap'>
{categoryOptions?.map(category => {
return (
<SmartLink
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<div
className={
'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
}>
<i className='mr-4 fas fa-folder' />
{category.name}({category.count})
</div>
</SmartLink>
)
})}
</div>
</div>
</>
)
}
/**
* 標籤列表
*/
const LayoutTagIndex = props => {
const { tagOptions } = props
const { locale } = useGlobal()
return (
<>
<div className='bg-white dark:bg-gray-700 py-10'>
<div className='dark:text-gray-200 mb-5'>
<i className='mr-4 fas fa-tag' />
{locale.COMMON.TAGS}:
</div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{tagOptions?.map(tag => {
return (
<div key={tag.name} className='p-2'>
<TagItemMini key={tag.name} tag={tag} />
</div>
)}
})}
</div>
</div>
</>
)
}
export { LayoutCategoryIndex, LayoutTagIndex }