Fix SSR hydration issues and finalize deployment config
Some checks failed
Deploy Documentation Site / build (push) Has been cancelled
Deploy Documentation Site / deploy (push) Has been cancelled

This commit is contained in:
hjdave
2026-04-04 16:59:37 +08:00
parent d6db7a3eef
commit ec9f4bbf02
6 changed files with 540 additions and 335 deletions

13
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
@@ -1629,6 +1630,18 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pg": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.14", "version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",

View File

@@ -18,6 +18,7 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",

View File

@@ -507,331 +507,12 @@ function HomeInner() {
</div> </div>
); );
} }
const [activeSection, setActiveSection] = useState("");
const [sidebarOpen, setSidebarOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [copied, setCopied] = useState(false);
const currentDoc = documentation.find((doc) => doc.slug === activeDoc); function Home() {
const sections = currentDoc?.sections || []; const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const handleCopy = () => { if (!mounted) return <div className="min-h-screen bg-zinc-50 dark:bg-zinc-900"></div>;
navigator.clipboard.writeText("npm install docs-site"); return <HomeInner />;
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleNavClick = (id: string) => {
setActiveSection(id);
setSidebarOpen(false);
const element = document.getElementById(id);
element?.scrollIntoView({ behavior: "smooth", block: "start" });
};
const filteredDocs = documentation.filter(
(doc) =>
doc.title.includes(searchQuery) ||
doc.description.includes(searchQuery)
);
return (
<div className={`${darkMode ? "dark" : ""} h-full`}>
<div className="flex h-full min-h-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100 transition-colors duration-200">
{/* 移动端遮罩 */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* 左侧导航栏 */}
<aside
className={`fixed lg:sticky top-0 left-0 z-50 h-screen w-72 bg-white dark:bg-zinc-800 border-r border-zinc-200 dark:border-zinc-700 transition-transform duration-300 ${
sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
}`}
>
<div className="p-6 h-full flex flex-col">
{/* Logo */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-8 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">D</span>
</div>
<h1 className="text-xl font-bold">DocsSite</h1>
</div>
</div>
{/* 搜索 */}
<div className="mb-6 relative">
<input
type="text"
placeholder="搜索文档..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-zinc-100 dark:bg-zinc-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
/>
<svg
className="absolute left-3 top-2.5 w-4 h-4 text-zinc-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{/* 文档列表 */}
<nav className="flex-1 overflow-y-auto">
<div className="space-y-1">
{filteredDocs.map((doc) => (
<button
key={doc.slug}
onClick={() => {
setActiveDoc(doc.slug);
setSidebarOpen(false);
}}
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
activeDoc === doc.slug
? "bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300"
: "hover:bg-zinc-100 dark:hover:bg-zinc-700"
}`}
>
<div className="font-medium text-sm">{doc.title}</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
{doc.description}
</div>
</button>
))}
</div>
</nav>
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 min-w-0">
{/* 顶部导航栏 */}
<header className="sticky top-0 z-30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-700">
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<h2 className="text-xl font-semibold">{currentDoc?.title}</h2>
</div>
<div className="flex items-center gap-2">
{!authenticated && (
<a
href="/admin"
className="px-3 py-1.5 bg-violet-500 hover:bg-violet-600 text-white text-sm rounded-lg transition-colors"
>
</a>
)}
{/* 深色模式切换 */}
<button
onClick={toggleTheme}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
title={theme === "light" ? "切换到深色模式" : "切换到浅色模式"}
>
{theme === "light" ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)}
</button>
{/* 复制链接 */}
<button
onClick={handleCopy}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors relative"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
{copied && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full text-white text-xs flex items-center justify-center">
</span>
)}
</button>
</div>
</div>
</header>
{/* 文档内容 */}
<div className="max-w-4xl mx-auto px-6 py-8">
{sections.map((section, index) => (
<section
key={section.title}
id={section.title}
className={`mb-12 scroll-mt-24`}
>
<div className="flex items-center justify-between mb-2">
<h3 className="text-2xl font-semibold">{section.title}</h3>
<a
href={`#${section.title}`}
className="opacity-0 hover:opacity-100 transition-opacity text-zinc-400 hover:text-violet-500"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</a>
</div>
{section.content && (
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed">
{section.content}
</p>
)}
{section.features && (
<div className="space-y-4">
{(section.features as string[] | FeatureItem[]).map((feature, i) => {
if (typeof feature === "string") {
return (
<div key={i} className="flex items-start gap-3">
<svg
className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>{feature}</span>
</div>
);
}
return (
<FeatureCard key={i} title={feature.title} description={feature.description}>
{feature.items && (
<ul className="space-y-2 mt-2">
{feature.items.map((item, j) => (
<li key={j} className="flex items-start gap-2 text-sm">
<span className="text-green-500 mt-0.5"></span>
{item}
</li>
))}
</ul>
)}
</FeatureCard>
);
})}
</div>
)}
{section.code && (
<CodeBlock language="bash">
{section.code}
</CodeBlock>
)}
{section.instructions && section.code && (
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed mt-4">
{section.instructions}
</p>
)}
</section>
))}
</div>
{/* 便签 */}
<div className="py-8">
<NotesList userId={auth.user?.id || null} />
</div>
{/* 留言板 */}
<div className="py-8 border-t border-zinc-200 dark:border-zinc-700">
<MessageBoard userId={auth.user?.id || null} username={auth.user?.username || null} />
</div>
{/* 底部 */}
<footer className="border-t border-zinc-200 dark:border-zinc-700 py-6 px-6">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<p className="text-sm text-zinc-500">
© 2026 DocsSite. All rights reserved.
</p>
<div className="flex items-center gap-4">
<a
href="#"
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
>
</a>
<a
href="#"
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
>
</a>
</div>
</div>
</footer>
</main>
</div>
</div>
);
} }
export default HomeInner; export default Home;

517
src/app/page.tsx.backup Normal file
View File

@@ -0,0 +1,517 @@
"use client";
import { useState, useEffect } from "react";
import CodeBlock from "@/components/CodeBlock";
import NotesList from "@/components/note/NotesList";
import MessageBoard from "@/components/message/MessageBoard";
import { useTheme } from "@/components/ThemeProvider";
import { useAuth } from "@/components/AuthProvider";
const documentation = [
{
title: "快速开始",
slug: "quick-start",
description: "了解如何开始使用我们的平台",
sections: [
{
title: "简介",
content: "欢迎使用我们的在线文档系统!这是一个现代、响应式、功能丰富的文档平台,专为技术文档设计。",
features: [
"响应式布局,支持移动端和桌面端",
"平滑滚动和锚点导航",
"代码高亮和复制功能",
"深色模式支持",
"内置搜索功能",
],
},
{
title: "安装",
content: "开始之前,请确保你已经安装了以下工具:",
requirements: ["Node.js (v18 或更高版本)", "npm 或 yarn 包管理器"],
code: "npm install\ngit pull origin main",
instructions: "然后运行以下命令:\n\nnpm install\n\n然后\n\nnpm run dev\n\n访问 http://localhost:3000 即可查看。",
},
{
title: "项目结构",
content: "项目采用以下目录结构:",
code: `docs-site/
├── src/
│ ├── app/
│ │ ├── page.tsx # 主文档页面
│ │ ├── layout.tsx # 根布局
│ │ └── globals.css # 全局样式
│ └── components/
│ ├── Sidebar.tsx # 侧边栏组件
│ ├── Navbar.tsx # 导航栏组件
│ └── CodeBlock.tsx # 代码块组件
├── public/
└── package.json`,
},
],
},
{
title: "核心功能",
slug: "features",
description: "探索平台的各项功能",
sections: [
{
title: "响应式布局",
content: "平台采用响应式设计,自动适配各种屏幕尺寸。",
features: [
{
title: "移动端",
items: ["侧边栏可折叠", "字体大小自动调整", "导航栏简化"],
},
{
title: "桌面端",
items: ["固定侧边栏", "更多内容展示", "优化排版"],
},
],
},
{
title: "交互功能",
content: "平台提供丰富的交互功能:",
features: [
{
title: "代码复制",
description: "点击代码块右上角的复制按钮即可快速复制代码",
},
{
title: "平滑滚动",
description: "点击导航链接自动滚动到对应位置",
},
{
title: "深色模式",
description: "点击右上角切换按钮即可切换主题",
},
{
title: "锚点导航",
description: "每个章节都有独立的锚点链接",
},
],
},
{
title: "搜索功能",
content: "内置搜索功能帮助你快速找到所需内容。",
code: "1. 点击右上角的搜索图标\n2. 输入关键词\n3. 查看搜索结果\n4. 点击结果跳转到对应章节",
instructions: "搜索支持标题、描述和代码片段。",
},
],
},
{
title: "API 参考",
slug: "api-reference",
description: "API 接口文档",
sections: [
{
title: "获取文档列表",
content: "获取所有可用文档的列表。",
code: `// GET /api/docs
Response:
{
"docs": [
{
"title": "文档标题",
"slug": "文档_slug",
"description": "文档描述"
}
]
}`,
},
{
title: "获取文档内容",
content: "获取指定文档的详细内容。",
code: `// GET /api/docs/:slug
Response:
{
"title": "文档标题",
"sections": [
{
"title": "章节标题",
"content": "章节内容"
}
]
}`,
},
],
},
];
interface FeatureItem {
title: string;
items?: string[];
description?: string;
}
function FeatureCard({ title, description, children }: {
title: string;
description?: string;
children?: React.ReactNode;
}) {
return (
<div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-4 mb-4 hover:shadow-lg transition-shadow">
<h4 className="font-semibold text-lg mb-2">{title}</h4>
{description && <p className="text-zinc-600 dark:text-zinc-400">{description}</p>}
{children}
</div>
);
}
function HomeInner() {
const { theme, toggleTheme } = useTheme();
const { authenticated, user, checkAuth } = useAuth();
const [activeDoc, setActiveDoc] = useState("quick-start");
const [activeSection, setActiveSection] = useState("");
const [sidebarOpen, setSidebarOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [copied, setCopied] = useState(false);
useEffect(() => {
checkAuth();
}, []);
const currentDoc = documentation.find((doc) => doc.slug === activeDoc);
const sections = currentDoc?.sections || [];
const handleCopy = () => {
navigator.clipboard.writeText("npm install docs-site");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleNavClick = (id: string) => {
setActiveSection(id);
setSidebarOpen(false);
const element = document.getElementById(id);
element?.scrollIntoView({ behavior: "smooth", block: "start" });
};
const filteredDocs = documentation.filter(
(doc) =>
doc.title.includes(searchQuery) ||
doc.description.includes(searchQuery)
);
return (
<div className={`${theme} h-full`}>
<div className="flex h-full min-h-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100 transition-colors duration-200">
{/* 移动端遮罩 */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* 左侧导航栏 */}
<aside
className={`fixed lg:sticky top-0 left-0 z-50 h-screen w-72 bg-white dark:bg-zinc-800 border-r border-zinc-200 dark:border-zinc-700 transition-transform duration-300 ${
sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
}`}
>
<div className="p-6 h-full flex flex-col">
{/* Logo */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-8 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">D</span>
</div>
<h1 className="text-xl font-bold">DocsSite</h1>
</div>
</div>
{/* 搜索 */}
<div className="mb-6 relative">
<input
type="text"
placeholder="搜索文档..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-zinc-100 dark:bg-zinc-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
/>
<svg
className="absolute left-3 top-2.5 w-4 h-4 text-zinc-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{/* 文档列表 */}
<nav className="flex-1 overflow-y-auto">
<div className="space-y-1">
{filteredDocs.map((doc) => (
<button
key={doc.slug}
onClick={() => {
setActiveDoc(doc.slug);
setSidebarOpen(false);
}}
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
activeDoc === doc.slug
? "bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300"
: "hover:bg-zinc-100 dark:hover:bg-zinc-700"
}`}
>
<div className="font-medium text-sm">{doc.title}</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
{doc.description}
</div>
</button>
))}
</div>
</nav>
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 min-w-0">
{/* 顶部导航栏 */}
<header className="sticky top-0 z-30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-700">
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<h2 className="text-xl font-semibold">{currentDoc?.title}</h2>
{!authenticated && (
<a
href="/admin"
className="px-3 py-1.5 bg-violet-500 hover:bg-violet-600 text-white text-sm rounded-lg transition-colors sm:hidden"
>
登录
</a>
)}
{authenticated && user && (
<div className="hidden sm:flex items-center gap-2 ml-auto">
<span className="text-sm text-zinc-500">{user.username}</span>
<button
onClick={async () => {
await fetch("/api/auth/logout", { method: "POST" });
await checkAuth();
}}
className="text-sm text-violet-500 hover:underline"
>
退出
</button>
</div>
)}
</div>
<div className="flex items-center gap-2">
{/* 深色模式切换 */}
<button
onClick={toggleTheme}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
title={theme === "light" ? "切换到深色模式" : "切换到浅色模式"}
>
{theme === "light" ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)}
</button>
{/* 复制链接 */}
<button
onClick={handleCopy}
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors relative"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
{copied && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full text-white text-xs flex items-center justify-center">
</span>
)}
</button>
</div>
</div>
</header>
{/* 文档内容 */}
<div className="max-w-4xl mx-auto px-6 py-8">
{sections.map((section, index) => (
<section
key={section.title}
id={section.title}
className={`mb-12 scroll-mt-24`}
>
<div className="flex items-center justify-between mb-2">
<h3 className="text-2xl font-semibold">{section.title}</h3>
<a
href={`#${section.title}`}
className="opacity-0 hover:opacity-100 transition-opacity text-zinc-400 hover:text-violet-500"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</a>
</div>
{section.content && (
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed">
{section.content}
</p>
)}
{section.features && (
<div className="space-y-4">
{(section.features as string[] | FeatureItem[]).map((feature, i) => {
if (typeof feature === "string") {
return (
<div key={i} className="flex items-start gap-3">
<svg
className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>{feature}</span>
</div>
);
}
return (
<FeatureCard key={i} title={feature.title} description={feature.description}>
{feature.items && (
<ul className="space-y-2 mt-2">
{feature.items.map((item, j) => (
<li key={j} className="flex items-start gap-2 text-sm">
<span className="text-green-500 mt-0.5">✓</span>
{item}
</li>
))}
</ul>
)}
</FeatureCard>
);
})}
</div>
)}
{section.code && (
<CodeBlock language="bash">
{section.code}
</CodeBlock>
)}
{section.instructions && section.code && (
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed mt-4">
{section.instructions}
</p>
)}
</section>
))}
{/* 便签 */}
<div className="py-8">
<NotesList userId={user?.id || null} />
</div>
{/* 留言板 */}
<div className="py-8 border-t border-zinc-200 dark:border-zinc-700">
<MessageBoard />
</div>
</div>
{/* 底部 */}
<footer className="border-t border-zinc-200 dark:border-zinc-700 py-6 px-6">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<p className="text-sm text-zinc-500">
© 2026 DocsSite. All rights reserved.
</p>
<div className="flex items-center gap-4">
<a
href="#"
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
>
隐私政策
</a>
<a
href="#"
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
>
联系方式
</a>
</div>
</div>
</footer>
</main>
</div>
</div>
);
function Home() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <div className="min-h-screen bg-zinc-50 dark:bg-zinc-900"></div>;
return <HomeInner />;
}
export default Home;

View File

@@ -42,13 +42,9 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
}); });
}; };
if (!mounted) {
return <>{children}</>;
}
return ( return (
<ThemeContext.Provider value={{ theme, toggleTheme }}> <ThemeContext.Provider value={{ theme, toggleTheme }}>
{children} {mounted ? children : <div>{children}</div>}
</ThemeContext.Provider> </ThemeContext.Provider>
); );
} }

View File

@@ -42,10 +42,7 @@ export async function getClient(): Promise<PoolClient> {
const client = await getPool().connect(); const client = await getPool().connect();
const release = () => client.release(); const release = () => client.release();
return { return client as PoolClient & { release: () => void };
...client,
release,
};
} }
// 初始化数据库表 // 初始化数据库表
@@ -213,7 +210,7 @@ export async function getDocs(): Promise<any[]> {
ORDER BY d.created_at DESC ORDER BY d.created_at DESC
` `
); );
return result.rows.map((row) => ({ return result.rows.map((row: any) => ({
title: row.title, title: row.title,
slug: row.slug, slug: row.slug,
description: row.description, description: row.description,