Fix SSR hydration issues and finalize deployment config
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
@@ -1629,6 +1630,18 @@
|
||||
"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": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
|
||||
331
src/app/page.tsx
331
src/app/page.tsx
@@ -507,331 +507,12 @@ function HomeInner() {
|
||||
</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);
|
||||
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={`${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>
|
||||
);
|
||||
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 HomeInner;
|
||||
export default Home;
|
||||
|
||||
517
src/app/page.tsx.backup
Normal file
517
src/app/page.tsx.backup
Normal 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;
|
||||
@@ -42,13 +42,9 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
});
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
{mounted ? children : <div>{children}</div>}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ export async function getClient(): Promise<PoolClient> {
|
||||
const client = await getPool().connect();
|
||||
const release = () => client.release();
|
||||
|
||||
return {
|
||||
...client,
|
||||
release,
|
||||
};
|
||||
return client as PoolClient & { release: () => void };
|
||||
}
|
||||
|
||||
// 初始化数据库表
|
||||
@@ -213,7 +210,7 @@ export async function getDocs(): Promise<any[]> {
|
||||
ORDER BY d.created_at DESC
|
||||
`
|
||||
);
|
||||
return result.rows.map((row) => ({
|
||||
return result.rows.map((row: any) => ({
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
description: row.description,
|
||||
|
||||
Reference in New Issue
Block a user