diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f62ec0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +# Keep environment variables out of version control +.env + +/generated/prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..e8b9fe9 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,15 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" + output = "../generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/webnotes/src/app/api/notes/[id]/route.ts b/webnotes/src/app/api/notes/[id]/route.ts index 0797272..8fca9c7 100644 --- a/webnotes/src/app/api/notes/[id]/route.ts +++ b/webnotes/src/app/api/notes/[id]/route.ts @@ -19,12 +19,23 @@ export async function PUT( const { id } = await params; const { htmlContent, textContent } = await request.json(); - const title = (textContent?.split('\n')[0]?.trim() || 'New Note').slice(0, 200); + + // Fetch the current note to check its title + const currentNote = await prisma.note.findUnique({ + where: { id } + }); + + let titleToUpdate = currentNote?.title; + + // Only update the title if it's still the default "New Note" or empty + if (currentNote && (currentNote.title === 'New Note' || !currentNote.title)) { + titleToUpdate = (textContent?.split('\n')[0]?.trim() || 'New Note').slice(0, 200); + } try { const result = await prisma.note.updateMany({ where: { id, userId }, // Ensures ownership - data: { title, content: htmlContent, updatedAt: new Date() }, + data: { title: titleToUpdate, content: htmlContent, updatedAt: new Date() }, }); if (result.count === 0) { @@ -71,4 +82,4 @@ export async function DELETE( } catch (error) { return NextResponse.json({ error: 'Failed to delete note' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/webnotes/src/app/components/NoteList.tsx b/webnotes/src/app/components/NoteList.tsx index e07a3a3..88712ce 100644 --- a/webnotes/src/app/components/NoteList.tsx +++ b/webnotes/src/app/components/NoteList.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import React from 'react'; import type { Note, Folder } from '@prisma/client'; import { FileText, Folder as FolderIcon, Trash2, ChevronRight, Edit } from 'lucide-react'; @@ -24,6 +24,8 @@ interface NoteListProps { toggleFolder: (folderId: string) => void; moveNote: (noteId: string, folderId: string | null) => void; onDataChange: () => void; // This replaces the need for deleteNote + newlyCreatedFolder?: { id: string; name: string } | null; // Added for inline folder creation + clearNewlyCreatedFolder?: () => void; // Added to clear the newly created folder state } function formatDate(date: Date | string) { @@ -45,15 +47,52 @@ export default function NoteList({ expandedFolders, toggleFolder, moveNote, - onDataChange // Receive the central data refresh function + onDataChange, // Receive the central data refresh function + newlyCreatedFolder, + clearNewlyCreatedFolder }: NoteListProps) { const [draggedNoteId, setDraggedNoteId] = useState(null); const [dragOverFolderId, setDragOverFolderId] = useState(null); + const [editingId, setEditingId] = useState(null); + const [newName, setNewName] = useState(''); + const inputRef = useRef(null); + + const newFolderInputCallbackRef = useCallback((node: HTMLInputElement) => { + if (node) { + node.focus(); + node.setSelectionRange(node.value.length, node.value.length); + } + }, []); + + // Effect to handle focusing when editing state changes + useEffect(() => { + if (editingId && !newlyCreatedFolder && inputRef.current) { + inputRef.current.focus(); + const length = newName.length; + inputRef.current.setSelectionRange(length, length); + } + }, [editingId, newlyCreatedFolder, newName]); + + // Effect to handle inline editing for newly created folders + useEffect(() => { + if (newlyCreatedFolder) { + setEditingId(newlyCreatedFolder.id); + setNewName(newlyCreatedFolder.name || 'New Folder'); + } + }, [newlyCreatedFolder]); + + // --- NEW: Handlers for Context Menu Actions --- - const handleRename = async (id: string, currentName: string, type: 'note' | 'folder') => { - const newName = prompt(`Rename ${type}:`, currentName); - if (newName && newName.trim() !== '' && newName !== currentName) { + const handleRename = (id: string, currentName: string) => { + setEditingId(id); + setNewName(currentName); + }; + + const confirmRename = async (id: string, type: 'note' | 'folder') => { + if (newName.trim() !== '' && newName !== (type === 'note' ? + unfiledNotes.find(n => n.id === id)?.title || 'Untitled' : + folders.find(f => f.id === id)?.name || 'Untitled')) { try { const res = await fetch(`/api/${type}s/${id}/rename`, { method: 'PATCH', @@ -66,6 +105,12 @@ export default function NoteList({ } else { throw new Error(); } } catch { toast.error(`Failed to rename ${type}.`); } } + setEditingId(null); + setNewName(''); + // Clear the newly created folder state if this was a newly created folder + if (newlyCreatedFolder?.id === id && clearNewlyCreatedFolder) { + clearNewlyCreatedFolder(); + } }; const handleDelete = async (id: string, type: 'note' | 'folder') => { @@ -111,62 +156,93 @@ export default function NoteList({ }; // In NoteList.tsx, update the renderNote function: -const renderNote = (note: Note, isIndented: boolean = false) => ( - {/* Move key here */} - -
handleDragStart(e, note.id)} - onDragEnd={handleDragEnd} - style={{ cursor: draggedNoteId ? 'grabbing' : 'grab' }} - className={isIndented ? 'ml-6' : ''} - > - setActiveNoteId(note.id)} - className={`flex items-start gap-3 p-2 rounded-md cursor-pointer transition-all relative group ${ - note.id === activeNoteId - ? 'bg-zinc-800 text-white' - : 'text-zinc-300 hover:bg-zinc-800 hover:text-white' - } ${draggedNoteId === note.id ? 'opacity-50' : ''}`} +const renderNote = (note: Note, isIndented: boolean = false) => { + const isEditing = editingId === note.id; + + return ( + {/* Move key here */} + +
handleDragStart(e, note.id)} + onDragEnd={handleDragEnd} + style={{ cursor: draggedNoteId ? 'grabbing' : 'grab' }} + className={isIndented ? 'ml-6' : ''} > - -
-

{note.title || 'Untitled'}

-

{formatDate(note.updatedAt)}

-
- - -
-
- - handleRename(note.id, note.title || 'Untitled', 'note')}> - Rename - - - handleDelete(note.id, 'note')}> - Delete - - -
-); + +
+ {isEditing ? ( + setNewName(e.target.value)} + onBlur={() => confirmRename(note.id, 'note')} + onKeyDown={(e) => { + if (e.key === 'Enter') { + confirmRename(note.id, 'note'); + } else if (e.key === 'Escape') { + setEditingId(null); + setNewName(''); + } + }} + className="bg-zinc-700 text-white rounded px-1 w-full" + aria-label="Rename note" + /> + ) : ( + <> +

{note.title || 'Untitled'}

+

{formatDate(note.updatedAt)}

+ + )} +
+ {!isEditing && ( + + )} +
+
+
+ {!isEditing && ( + + handleRename(note.id, note.title || 'Untitled')}> + Rename + + + handleDelete(note.id, 'note')}> + Delete + + + )} +
+ ); +}; // Update the renderFolder function similarly: const renderFolder = (folder: Folder & { notes?: Note[] }) => { const isExpanded = expandedFolders.has(folder.id); const folderNotes = notesInFolders.get(folder.id) || []; const isDragOver = dragOverFolderId === folder.id; + const isEditing = editingId === folder.id || newlyCreatedFolder?.id === folder.id; return ( {/* Move key here */} @@ -185,7 +261,7 @@ const renderFolder = (folder: Folder & { notes?: Note[] }) => { animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: -30 }} transition={{ duration: 0.2 }} - onClick={() => toggleFolder(folder.id)} + onClick={() => !isEditing && toggleFolder(folder.id)} className={`flex items-center gap-2 p-2 rounded-md text-zinc-300 hover:bg-zinc-800 hover:text-white cursor-pointer ${ isDragOver ? 'bg-zinc-800/50' : '' }`} @@ -194,12 +270,36 @@ const renderFolder = (folder: Folder & { notes?: Note[] }) => { - {folder.name} - {folderNotes.length} +
+ {isEditing ? ( + setNewName(e.target.value)} + onBlur={() => confirmRename(folder.id, 'folder')} + onKeyDown={(e) => { + if (e.key === 'Enter') { + confirmRename(folder.id, 'folder'); + } else if (e.key === 'Escape') { + setEditingId(null); + setNewName(''); + } + }} + className="bg-zinc-700 text-white rounded px-1 w-full" + aria-label="Rename folder" + /> + ) : ( + {folder.name} + )} +
+ {!isEditing && ( + {folderNotes.length} + )} - {isExpanded && ( + {isExpanded && !isEditing && ( { - - handleRename(folder.id, folder.name, 'folder')}> - Rename - - handleDelete(folder.id, 'folder')}> - Delete - - + {!isEditing && ( + + handleRename(folder.id, folder.name)}> + Rename + + handleDelete(folder.id, 'folder')}> + Delete + + + )}
); }; diff --git a/webnotes/src/app/components/Sidebar.tsx b/webnotes/src/app/components/Sidebar.tsx index 254d422..9d05a0e 100644 --- a/webnotes/src/app/components/Sidebar.tsx +++ b/webnotes/src/app/components/Sidebar.tsx @@ -22,9 +22,8 @@ interface SidebarProps { activeNoteId: string | null; setActiveNoteId: (id: string) => void; createNote: (folderId?: string | null) => void; - deleteNote: (id: string) => void; moveNote: (noteId: string, folderId: string | null) => void; - createFolder: () => void; + createFolder: () => Promise<{ id: string; name: string } | null>; // Modified to return folder object onDataChange: () => void; } @@ -34,13 +33,23 @@ export default function Sidebar({ activeNoteId, setActiveNoteId, createNote, - deleteNote, moveNote, createFolder, onDataChange }: SidebarProps) { const [isOpen, setIsOpen] = useState(true); const [expandedFolders, setExpandedFolders] = useState>(new Set()); + + + const [newlyCreatedFolder, setNewlyCreatedFolder] = useState<{ id: string; name: string } | null>(null); + + const handleCreateFolder = async () => { + const newFolder = await createFolder(); + if (newFolder) { + setNewlyCreatedFolder(newFolder); + // Don't call onDataChange immediately - let the NoteList handle it after rename + } + }; useEffect(() => { const saved = localStorage.getItem('expandedFolders'); @@ -104,27 +113,33 @@ export default function Sidebar({
- -

New Note

+ +

New Note

+
- -

New Folder

+ +

New Folder

+
- -

Search

+ +

Search

+
@@ -140,6 +155,8 @@ export default function Sidebar({ toggleFolder={toggleFolder} moveNote={moveNote} onDataChange={onDataChange} + newlyCreatedFolder={newlyCreatedFolder} + clearNewlyCreatedFolder={() => setNewlyCreatedFolder(null)} // Removed deleteNote - it's handled by onDataChange in NoteList /> @@ -151,4 +168,4 @@ export default function Sidebar({ ); -} \ No newline at end of file +} diff --git a/webnotes/src/app/components/context-menu.tsx b/webnotes/src/app/components/context-menu.tsx index 1306dd2..5c98bae 100644 --- a/webnotes/src/app/components/context-menu.tsx +++ b/webnotes/src/app/components/context-menu.tsx @@ -62,7 +62,7 @@ const ContextMenuContent = React.forwardRef< { fetchData(); - }, []); + }, [fetchData]); const handleDataChange = useCallback(() => { fetchData(); @@ -72,20 +72,7 @@ export default function Home() { } }; - const deleteNote = async (id: string) => { - const originalNotes = [...notes]; - const newNotes = notes.filter((note) => note.id !== id); - setNotes(newNotes); - if (activeNoteId === id) setActiveNoteId(newNotes.length > 0 ? newNotes[0].id : null); - - const res = await fetch(`/api/notes/${id}`, { method: 'DELETE' }); - if (!res.ok) { - setNotes(originalNotes); - toast.error("Failed to delete note."); - } else { - handleDataChange(); - } - }; + const moveNote = async (noteId: string, folderId: string | null) => { const originalNotes = [...notes]; @@ -106,15 +93,19 @@ export default function Home() { }; const createFolder = async () => { - const folderName = prompt("Enter folder name:"); - if (folderName) { - const res = await fetch('/api/folders', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ name: folderName }) - }); - if (res.ok) handleDataChange(); + const res = await fetch('/api/folders', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ name: 'New Folder' }) // Create with a default name + }); + + if (res.ok) { + const { folder } = await res.json(); // Destructure the folder object + handleDataChange(); + // Return the newly created folder object + return folder; } + return null; }; const handleNoteUpdate = (updatedNote: Note) => { @@ -139,7 +130,6 @@ export default function Home() { activeNoteId={activeNoteId} setActiveNoteId={setActiveNoteId} createNote={createNote} - deleteNote={deleteNote} moveNote={moveNote} createFolder={createFolder} onDataChange={handleDataChange} // Added this line @@ -150,4 +140,4 @@ export default function Home() { ); -} \ No newline at end of file +}