import { useState, useRef, useEffect, memo } from "react"; import { createRoute } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { api, useApiQuery } from "@/lib/api"; import { useRealtime } from "@/hooks/use-realtime"; import { Plus, Trash2, Search, Pin, FileText, Link as LinkIcon, History } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import type { Note, PaginatedResponse } from "@/lib/types"; // Simple TipTap-like editor using contentEditable - saves on blur only const NoteEditor = memo(function NoteEditor({ initialContent, onSave, placeholder = "Start writing..." }: { initialContent: string; onSave: (html: string) => void; placeholder?: string }) { const editorRef = useRef(null); const [isPlaceholder, setIsPlaceholder] = useState(!initialContent); useEffect(() => { if (editorRef.current && !editorRef.current.innerHTML) { editorRef.current.innerHTML = initialContent || ""; } setIsPlaceholder(!initialContent); }, []); const handleBlur = () => { const html = editorRef.current?.innerHTML || ""; onSave(html); }; return (
{isPlaceholder && (
{placeholder}
)}
); }); // Completely uncontrolled title input - uses ref to avoid any re-render const NoteTitleInput = memo(function NoteTitleInput({ noteId, initialTitle }: { noteId: string; initialTitle: string }) { const inputRef = useRef(null); const queryClient = useQueryClient(); useEffect(() => { if (inputRef.current) { inputRef.current.value = initialTitle; } }, [initialTitle, noteId]); const handleBlur = () => { const newTitle = inputRef.current?.value || ""; if (newTitle !== initialTitle) { api.patch("/notes/" + noteId, { title: newTitle }).then(() => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }); } }; return ( ); }); function NotesPage() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); const [selectedNoteId, setSelectedNoteId] = useState(null); const [showBacklinks, setShowBacklinks] = useState(false); const [showVersions, setShowVersions] = useState(false); const selectedNoteRef = useRef(null); useRealtime({ enabled: true }); const { data: notesData, isLoading } = useApiQuery>( ["notes", search], "/notes?limit=200" + (search ? "&search=" + encodeURIComponent(search) : "") ); const notes = notesData?.items || []; const createMutation = useMutation({ mutationFn: () => api.post("/notes", { title: "Untitled", content: "" }), onSuccess: (note) => { queryClient.invalidateQueries({ queryKey: ["notes"] }); selectedNoteRef.current = note; setSelectedNoteId(note.id); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/notes/" + id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete("/notes/" + id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); selectedNoteRef.current = null; setSelectedNoteId(null); }, }); const selectNote = async (note: Note) => { try { const detail = await api.get("/notes/" + note.id); selectedNoteRef.current = detail; } catch { selectedNoteRef.current = note; } setSelectedNoteId(note.id); setShowBacklinks(false); setShowVersions(false); }; const selectedNote = selectedNoteRef.current; return (
{/* Left pane - note list */}
setSearch(e.target.value)} className="pl-8" tabIndex={-1} />
{isLoading ? (
Loading...
) : notes.length === 0 ? (
No notes yet.
) : (
{notes.map((note) => ( ))}
)}
{/* Right pane - editor */}
{selectedNote ? ( <>
Delete Note Are you sure you want to delete "{selectedNote.title}"? Cancel deleteMutation.mutate(selectedNote.id)} className="bg-destructive text-destructive-foreground">Delete
{ if (selectedNoteRef.current) { updateMutation.mutate({ id: selectedNoteRef.current.id, data: { content: html } }); } }} />
{/* Backlinks section */} {showBacklinks && selectedNote.backlinks && selectedNote.backlinks.length > 0 && (

Linked from

{selectedNote.backlinks.map((bl) => (
{bl.noteTitle}
))}
)} {/* Version history */} {showVersions && (

Version History

Version history available via API.

)} ) : (

Select a note or create a new one

)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/notes", component: NotesPage, });