| import { useState } from "react"; | |
| import { FileText, Plus, Save, Trash2 } from "lucide-react"; | |
| interface Note { | |
| id: string; | |
| title: string; | |
| body: string; | |
| } | |
| interface NotesPanelProps { | |
| notes?: Note[]; | |
| onSave?: (notes: Note[]) => void | Promise<void>; | |
| } | |
| const STORAGE_KEY = "ie-workspace-notes"; | |
| const initialNote: Note = { id: "note-1", title: "Research note", body: "" }; | |
| export function NotesPanel({ notes, onSave }: NotesPanelProps) { | |
| const [items, setItems] = useState<Note[]>(() => { | |
| if (notes?.length) return notes; | |
| const saved = localStorage.getItem(STORAGE_KEY); | |
| if (saved) { | |
| try { | |
| return JSON.parse(saved); | |
| } catch { | |
| // Ignore | |
| } | |
| } | |
| return [initialNote]; | |
| }); | |
| const [activeId, setActiveId] = useState(() => { | |
| return items[0]?.id || initialNote.id; | |
| }); | |
| const active = items.find((note) => note.id === activeId) || items[0]; | |
| const updateActive = (patch: Partial<Note>) => { | |
| setItems((current) => { | |
| const next = current.map((note) => note.id === active.id ? { ...note, ...patch } : note); | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); | |
| onSave?.(next); | |
| return next; | |
| }); | |
| }; | |
| const addNote = () => { | |
| const next: Note = { id: `note-${Date.now()}`, title: "Untitled note", body: "" }; | |
| setItems((current) => { | |
| const updated = [...current, next]; | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); | |
| onSave?.(updated); | |
| return updated; | |
| }); | |
| setActiveId(next.id); | |
| }; | |
| const removeNote = (id: string) => { | |
| setItems((current) => { | |
| const next = current.filter((note) => note.id !== id); | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); | |
| onSave?.(next); | |
| setActiveId(next[0]?.id || ""); | |
| return next; | |
| }); | |
| }; | |
| return ( | |
| <section className="flex h-full min-h-[320px] bg-white/30 dark:bg-slate-950/20 backdrop-blur-md"> | |
| <aside className="w-48 border-r border-white/10 dark:border-white/5 p-3 flex flex-col gap-2"> | |
| <button | |
| onClick={addNote} | |
| className="flex w-full items-center justify-center gap-2 rounded-xl bg-teal-600 hover:bg-teal-700 active:scale-95 text-white py-2 text-xs font-bold transition duration-200 shadow-md shadow-teal-600/10" | |
| > | |
| <Plus className="h-4 w-4" /> New Note | |
| </button> | |
| <div className="flex-1 overflow-y-auto space-y-1.5 custom-scrollbar"> | |
| {items.map((note) => ( | |
| <button | |
| key={note.id} | |
| onClick={() => setActiveId(note.id)} | |
| className={`w-full rounded-xl px-3.5 py-2.5 text-left text-xs font-bold transition-all duration-200 block border ${ | |
| note.id === activeId | |
| ? "bg-teal-600 border-teal-500 text-white shadow-md shadow-teal-600/15" | |
| : "bg-white/40 dark:bg-slate-900/40 border-transparent hover:bg-white/60 dark:hover:bg-slate-800/60 text-slate-700 dark:text-slate-300" | |
| }`} | |
| > | |
| <div className="truncate font-semibold text-sm">{note.title || "Untitled note"}</div> | |
| <div className={`text-[10px] mt-1 truncate ${note.id === activeId ? 'text-teal-100/80' : 'text-slate-400 dark:text-slate-500'}`}> | |
| {note.body ? note.body.substring(0, 30) : "No additional text"} | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </aside> | |
| {active ? ( | |
| <div className="flex min-w-0 flex-1 flex-col"> | |
| <header className="flex items-center gap-3 border-b border-white/10 dark:border-white/5 px-4 py-3 bg-white/20 dark:bg-slate-900/20"> | |
| <FileText className="h-4.5 w-4.5 text-teal-500 shrink-0" /> | |
| <input | |
| value={active.title} | |
| onChange={(event) => updateActive({ title: event.target.value })} | |
| className="min-w-0 flex-1 bg-transparent text-sm font-bold text-slate-850 dark:text-white outline-none" | |
| placeholder="Note Title" | |
| /> | |
| <button | |
| onClick={() => { | |
| onSave?.(items); | |
| }} | |
| className="rounded-xl p-2 hover:bg-white/40 dark:hover:bg-slate-800/40 transition active:scale-95 text-slate-500 hover:text-slate-800 dark:hover:text-white" | |
| title="Save notes" | |
| > | |
| <Save className="h-4 w-4" /> | |
| </button> | |
| <button | |
| onClick={() => removeNote(active.id)} | |
| className="rounded-xl p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/30 transition active:scale-95" | |
| title="Delete note" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </button> | |
| </header> | |
| <textarea | |
| value={active.body} | |
| onChange={(event) => updateActive({ body: event.target.value })} | |
| className="min-h-0 flex-1 resize-none bg-transparent p-5 text-xs text-slate-700 dark:text-slate-350 outline-none leading-relaxed" | |
| placeholder="Start writing..." | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-1 flex-col items-center justify-center text-slate-400 p-8"> | |
| <div className="text-4xl mb-3">📓</div> | |
| <p className="text-sm font-bold">No note selected</p> | |
| <p className="text-xs text-slate-500 mt-1">Create a note to start capturing your ideas.</p> | |
| </div> | |
| )} | |
| </section> | |
| ); | |
| } | |
| export default NotesPanel; | |
Xet Storage Details
- Size:
- 5.51 kB
- Xet hash:
- b16404617fad08f972c97970c3e6bb0d6ef4c0bafb0b0df9d2c3b40f499480f6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.