Files
docs-site/src/components/note/NotesList.tsx

250 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}