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
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:
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user