'use client';
import { useEffect, useState, Suspense } from 'react';
import { Plus, FileText, Link2, GitBranch } from 'lucide-react';
import dynamic from 'next/dynamic';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { DailyNoteButton } from '@/components/notes/daily-note-button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
// Lazy load TipTap editor (~80KB TipTap + extensions)
const NoteEditor = dynamic(
() => import('@/components/notes/note-editor').then((m) => m.NoteEditor),
{
ssr: false,
loading: () => (
),
}
);
// Lazy load react-force-graph-2d (~120KB + three.js)
const NoteGraph = dynamic(
() => import('@/components/notes/note-graph').then((m) => m.NoteGraph),
{
ssr: false,
loading: () => (
),
}
);
interface Note {
id: string;
title: string;
content: string;
domain: string;
created: string;
updated: string;
}
interface Backlink {
id: string;
title: string;
}
export default function NotesPage() {
const [notes, setNotes] = useState([]);
const [selectedNote, setSelectedNote] = useState(null);
const [backlinks, setBacklinks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchNotes();
}, []);
useEffect(() => {
if (selectedNote) {
fetchBacklinks(selectedNote.id);
}
}, [selectedNote]);
async function fetchNotes() {
try {
const response = await fetch('/api/notes?sort=-updated');
if (response.ok) {
const data = await response.json();
const notesList = data.items || [];
setNotes(notesList);
if (notesList.length > 0 && !selectedNote) {
setSelectedNote(notesList[0]);
}
}
} catch (error) {
console.error('Failed to fetch notes:', error);
} finally {
setLoading(false);
}
}
async function fetchBacklinks(noteId: string) {
try {
const response = await fetch(`/api/notes/${noteId}/backlinks`);
if (response.ok) {
const data = await response.json();
setBacklinks(data.items || []);
}
} catch (error) {
console.error('Failed to fetch backlinks:', error);
}
}
async function createNote() {
try {
const response = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Untitled note',
content: '',
domain: 'personal',
}),
});
if (response.ok) {
const newNote = await response.json();
setNotes([newNote, ...notes]);
setSelectedNote(newNote);
}
} catch (error) {
console.error('Failed to create note:', error);
}
}
function handleDailyNoteReady(raw: Record) {
const note = raw as unknown as Note;
// If the note already appears in the list, just select it
const exists = notes.find((n) => n.id === note.id);
if (exists) {
setSelectedNote(exists);
return;
}
// Otherwise prepend it and select
setNotes([note, ...notes]);
setSelectedNote(note);
}
async function updateNote(noteId: string, updates: Partial) {
try {
await fetch(`/api/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
fetchNotes();
} catch (error) {
console.error('Failed to update note:', error);
}
}
async function deleteNote(noteId: string) {
if (!confirm('Are you sure you want to delete this note?')) return;
try {
await fetch(`/api/notes/${noteId}`, { method: 'DELETE' });
const updatedNotes = notes.filter((n) => n.id !== noteId);
setNotes(updatedNotes);
if (selectedNote?.id === noteId) {
setSelectedNote(updatedNotes[0] || null);
}
} catch (error) {
console.error('Failed to delete note:', error);
}
}
if (loading) {
return Loading notes...
;
}
return (
Notes
Connect ideas to the work they shape.
{/* Notes list */}
{notes.length === 0 ? (
No notes yet
) : (
{notes.map((note) => (
))}
)}
{/* Note editor */}
{selectedNote ? (
) : (
Select a note or create a new one
)}
{/* Backlinks and graph */}
Backlinks
Graph
{backlinks.length === 0 ? (
No backlinks
) : (
{backlinks.map((link) => (
))}
)}
Loading graph...
}
>
);
}