Initial commit: Online documentation site with CMS, notes, and message board

This commit is contained in:
hjdave
2026-04-04 16:00:52 +08:00
commit 38dc7203d1
34 changed files with 9706 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
"use client";
import { useState, useEffect } from "react";
import { useAuth } from "@/components/AuthProvider";
interface Note {
id: number;
title: string;
content: string;
color: string;
is_pinned: boolean;
created_at: string;
}
interface NotesListProps {
userId: number | null;
}
export default function NotesList({ userId }: NotesListProps) {
const [notes, setNotes] = useState<Note[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
const [formData, setFormData] = useState({
title: "",
content: "",
color: "#fef08a",
isPinned: false,
});
const { authenticated, user } = useAuth();
const colors = [
"#fef08a", // yellow
"#86efac", // green
"#93c5fd", // blue
"#fda4af", // pink
"#c4b5fd", // purple
"#fb923c", // orange
];
useEffect(() => {
if (userId) {
fetchNotes();
}
}, [userId]);
const fetchNotes = async () => {
try {
const res = await fetch("/api/notes");
const data = await res.json();
if (data.notes) {
setNotes(data.notes);
}
} catch (error) {
console.error("Failed to fetch notes:", error);
}
};
const handleAddNote = async () => {
try {
const res = await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
const result = await res.json();
if (result.success) {
setFormData({ title: "", content: "", color: "#fef08a", isPinned: false });
setShowAddForm(false);
fetchNotes();
}
} catch (error) {
console.error("Failed to add note:", error);
}
};
const handleDeleteNote = async (noteId: number) => {
try {
const res = await fetch(`/api/notes?id=${noteId}`, { method: "DELETE" });
const result = await res.json();
if (result.success) {
fetchNotes();
}
} catch (error) {
console.error("Failed to delete note:", error);
}
};
const handleTogglePin = async (noteId: number) => {
try {
const res = await fetch("/api/notes/toggle-pin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: noteId }),
});
const result = await res.json();
if (result.success) {
fetchNotes();
}
} catch (error) {
console.error("Failed to toggle pin:", error);
}
};
if (!userId) {
return (
<div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-6 text-center text-zinc-500">
便
</div>
);
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">便</h2>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">便</h2>
{authenticated && (
<button
onClick={() => setShowAddForm(!showAddForm)}
className="px-4 py-2 bg-violet-500 hover:bg-violet-600 text-white rounded-lg transition-colors"
>
{showAddForm ? "取消" : "+ 添加便签"}
</button>
)}
</div>
</div>
{showAddForm && (
<div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-6 mb-6 space-y-4">
<div>
<label className="block text-sm text-zinc-400 mb-2"></label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-4 py-2 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg focus:outline-none focus:border-violet-500"
placeholder="便签标题"
/>
</div>
<div>
<label className="block text-sm text-zinc-400 mb-2"></label>
<textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
className="w-full px-4 py-2 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-lg focus:outline-none focus:border-violet-500"
placeholder="便签内容"
rows={4}
/>
</div>
<div>
<label className="block text-sm text-zinc-400 mb-2"></label>
<div className="flex gap-2">
{colors.map((color) => (
<button
key={color}
onClick={() => setFormData({ ...formData, color })}
className={`w-8 h-8 rounded-lg ${color === formData.color ? "ring-2 ring-violet-500 ring-offset-2" : ""}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="isPinned"
checked={formData.isPinned}
onChange={(e) => setFormData({ ...formData, isPinned: e.target.checked })}
className="rounded"
/>
<label htmlFor="isPinned" className="text-sm">
</label>
</div>
<button
onClick={handleAddNote}
disabled={!formData.title}
className="w-full px-4 py-2 bg-violet-500 hover:bg-violet-600 disabled:bg-zinc-300 dark:disabled:bg-zinc-700 text-white rounded-lg transition-colors"
>
便
</button>
</div>
)}
{authenticated && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{notes.map((note) => (
<div
key={note.id}
className="rounded-lg p-4 shadow-md hover:shadow-lg transition-shadow relative group"
style={{ backgroundColor: note.color }}
>
<div className="flex items-start justify-between mb-2">
<h3 className="font-semibold text-zinc-900 line-clamp-2">{note.title}</h3>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleTogglePin(note.id)}
className="p-1 hover:bg-black/20 rounded"
title={note.is_pinned ? "取消置顶" : "置顶"}
>
<svg className="w-4 h-4 text-zinc-800" fill={note.is_pinned ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5l2 2 3-3 5 5 5-5 2 2-7 7-5-5-3 3-2-2z" />
</svg>
</button>
<button
onClick={() => handleDeleteNote(note.id)}
className="p-1 hover:bg-red-500 hover:text-white rounded"
title="删除"
>
<svg className="w-4 h-4 text-zinc-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<p className="text-zinc-800 text-sm line-clamp-4 mb-3">{note.content}</p>
<p className="text-xs text-zinc-600">
{new Date(note.created_at).toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
))}
{notes.length === 0 && (
<div className="col-span-full text-center py-12 text-zinc-500">
便"添加便签"
</div>
)}
</div>
)}
{!authenticated && (
<div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-6 text-center text-zinc-500">
便
</div>
)}
</div>
);
}