feat: hierarchical notebooks (tree), remove all list view code, delete 22 unused files
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m3s

- Add parentId to Notebook model (tree structure)
- Update sidebar to render parent/child notebooks with expand/collapse
- Add sub-notebook creation from parent notebook
- Remove 'list' from NotesViewMode type everywhere
- Delete 22 unused components, hooks, and UI files
- Wrap revalidatePath in try-catch to prevent save 500
- Update notebook API to support parentId in creation
This commit is contained in:
Antigravity
2026-05-09 21:02:23 +00:00
parent 5a6ec4808f
commit d90b29b34f
30 changed files with 155 additions and 4280 deletions

View File

@@ -82,6 +82,7 @@ function SidebarCarnetItem({
activeNoteId,
onCarnetClick,
onNoteClick,
children,
isDragging,
dragHandleProps,
}: {
@@ -91,6 +92,7 @@ function SidebarCarnetItem({
activeNoteId: string | null
onCarnetClick: () => void
onNoteClick: (noteId: string, carnetId: string) => void
children?: React.ReactNode
isDragging?: boolean
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
}) {
@@ -98,7 +100,6 @@ function SidebarCarnetItem({
return (
<div className={cn('space-y-1 transition-opacity', isDragging && 'opacity-40')}>
<div className="relative group/carnet">
{/* Drag handle — visible on hover */}
<div
{...dragHandleProps}
className="absolute left-1 top-1/2 -translate-y-1/2 p-1 rounded text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover/carnet:opacity-100 transition-opacity z-10"
@@ -152,6 +153,7 @@ function SidebarCarnetItem({
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden space-y-0.5"
>
{children}
{notes.map(note => (
<NoteLink
key={note.id}
@@ -160,7 +162,7 @@ function SidebarCarnetItem({
onClick={() => onNoteClick(note.id, carnet.id)}
/>
))}
{notes.length === 0 && (
{notes.length === 0 && !children && (
<p className="pl-12 text-[11px] text-muted-foreground/50 py-2 italic font-light">{t('common.noResults')}</p>
)}
</motion.div>
@@ -178,18 +180,30 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const { notebooks, updateNotebookOrderOptimistic } = useNotebooks()
const { refreshKey } = useNoteRefresh()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [createParentId, setCreateParentId] = useState<string | null>(null)
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string }[]>>({})
const [activeView, setActiveView] = useState<NavigationView>('notebooks')
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
const [showSortMenu, setShowSortMenu] = useState(false)
// ── Drag state ──
const [draggedId, setDraggedId] = useState<string | null>(null)
const [orderedNotebooks, setOrderedNotebooks] = useState<Notebook[]>([])
const dragOverId = useRef<string | null>(null)
// Prevents the sync effect from overwriting a just-saved drag order
const isSavingRef = useRef(false)
const rootNotebooks = useMemo(() => orderedNotebooks.filter(nb => !nb.parentId), [orderedNotebooks])
const childNotebooks = useMemo(() => {
const map = new Map<string, Notebook[]>()
for (const nb of orderedNotebooks) {
if (nb.parentId) {
const children = map.get(nb.parentId) || []
children.push(nb)
map.set(nb.parentId, children)
}
}
return map
}, [orderedNotebooks])
const currentNotebookId = searchParams.get('notebook')
const currentNoteId = searchParams.get('openNote')
@@ -548,10 +562,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
>
{orderedNotebooks.map((notebook: Notebook) => {
{rootNotebooks.map((notebook: Notebook) => {
const isActive = currentNotebookId === notebook.id
const notes = notebookNotes[notebook.id] || []
const isDragging = draggedId === notebook.id
const children = childNotebooks.get(notebook.id) || []
const isChildActive = children.some(c => currentNotebookId === c.id)
const isExpanded = isActive || isChildActive
return (
<motion.div
key={notebook.id}
@@ -575,20 +592,58 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
name: notebook.name,
initial: notebook.name.charAt(0).toUpperCase(),
}}
isActive={isActive}
isActive={isExpanded}
notes={notes}
activeNoteId={currentNoteId}
onCarnetClick={() => handleCarnetClick(notebook.id)}
onNoteClick={handleNoteClick}
isDragging={isDragging}
/>
>
{children.length > 0 && (
<div className="pl-4 space-y-1 mt-1">
{children.map(child => {
const childActive = currentNotebookId === child.id
const childNotes = notebookNotes[child.id] || []
return (
<div key={child.id}>
<SidebarCarnetItem
carnet={{
id: child.id,
name: child.name,
initial: child.name.charAt(0).toUpperCase(),
}}
isActive={childActive}
notes={childNotes}
activeNoteId={currentNoteId}
onCarnetClick={() => handleCarnetClick(child.id)}
onNoteClick={handleNoteClick}
/>
</div>
)
})}
<button
onClick={() => {
setCreateParentId(notebook.id)
setIsCreateDialogOpen(true)
}}
className="w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[11px] text-muted-foreground/50 hover:text-muted-foreground transition-colors"
>
<Plus size={12} />
<span>{t('notebook.createSubNotebook') || 'Sous-carnet'}</span>
</button>
</div>
)}
</SidebarCarnetItem>
</div>
</motion.div>
)
})}
<button
onClick={() => setIsCreateDialogOpen(true)}
onClick={() => {
setCreateParentId(null)
setIsCreateDialogOpen(true)
}}
className="w-full mt-4 flex items-center gap-3 px-4 py-2 text-[13px] text-muted-foreground hover:text-foreground transition-colors font-medium rounded-lg hover:bg-white/40"
>
<Plus size={16} />
@@ -669,6 +724,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<CreateNotebookDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
parentNotebookId={createParentId}
/>
</>
)