Initial commit: Online documentation site with CMS, notes, and message board
This commit is contained in:
249
src/components/note/NotesList.tsx
Normal file
249
src/components/note/NotesList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user