From 0470f044ef08a8a9c62ef343ec1594412335fad6 Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 27 May 2026 19:55:23 -0600 Subject: [PATCH] refactor(doclist): restructure loading and empty states, update skeletons and columns UI Simplify document list state handling by centralizing loading and empty logic into DocumentList. Remove redundant state management from HomeContent. Refactor DocumentListSkeleton to support all view modes and icon sizes for improved visual consistency during loading. Enhance ColumnsView styling for folder and document rows, improving selection, hover, and drag feedback. Use useMemo for visible order calculation to optimize rendering. These changes streamline the document list rendering flow, unify skeleton loading visuals, and improve the clarity and responsiveness of the columns view interface. --- src/components/HomeContent.tsx | 29 --- src/components/doclist/DocumentList.tsx | 126 ++++++------ .../doclist/DocumentListSkeleton.tsx | 189 ++++++++++++++---- src/components/doclist/views/ColumnsView.tsx | 62 ++++-- 4 files changed, 259 insertions(+), 147 deletions(-) diff --git a/src/components/HomeContent.tsx b/src/components/HomeContent.tsx index 138f3c3..6cc2be8 100644 --- a/src/components/HomeContent.tsx +++ b/src/components/HomeContent.tsx @@ -1,12 +1,8 @@ 'use client'; -import { Header } from '@/components/Header'; -import { DocumentUploader } from '@/components/documents/DocumentUploader'; import { DocumentList } from '@/components/doclist/DocumentList'; -import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton'; import { SettingsModal } from '@/components/SettingsModal'; import { UserMenu } from '@/components/auth/UserMenu'; -import { useDocuments } from '@/contexts/DocumentContext'; const Brand = () => (
@@ -26,31 +22,6 @@ const AppActions = () => ( ); export function HomeContent() { - const { pdfDocs, epubDocs, htmlDocs, isPDFLoading } = useDocuments(); - const totalDocs = (pdfDocs?.length || 0) + (epubDocs?.length || 0) + (htmlDocs?.length || 0); - - if (isPDFLoading) { - return ( -
-
} right={} /> -
- -
-
- ); - } - - if (totalDocs === 0) { - return ( -
-
} right={} /> -
- -
-
- ); - } - return (
} appActions={} /> diff --git a/src/components/doclist/DocumentList.tsx b/src/components/doclist/DocumentList.tsx index 0ccbe9e..663120e 100644 --- a/src/components/doclist/DocumentList.tsx +++ b/src/components/doclist/DocumentList.tsx @@ -409,17 +409,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { [allDocuments], ); - if (isPDFLoading || isEPUBLoading || isHTMLLoading) { - return ; - } - - if (allDocuments.length === 0) { - return ( -
- -
- ); - } + const isLoading = isPDFLoading || isEPUBLoading || isHTMLLoading; const fallbackViewMode: ViewMode = viewMode === 'columns' && isNarrow ? 'list' : viewMode; @@ -473,7 +463,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { sidebarOpen={sidebarOpen} onSidebarOpenChange={setSidebarOpen} > - {showHint && allDocuments.length > 1 && ( + {!isLoading && showHint && allDocuments.length > 1 && (

@@ -493,57 +483,67 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {

)} - - {fallbackViewMode === 'icons' && ( - - )} - {fallbackViewMode === 'list' && ( - { - setSortBy(b); - setSortDirection(d); - }} - collapsedFolders={collapsedFolders} - onToggleCollapse={toggleFolderCollapse} - onDeleteFolder={handleDeleteFolder} - onDeleteDoc={handleDeleteDoc} - onDropOnFolder={handleDropOnFolder} - onMergeIntoFolder={handleMergeIntoFolder} - /> - )} - {fallbackViewMode === 'columns' && ( - - )} - {fallbackViewMode === 'gallery' && ( - - )} - + {isLoading ? ( +
+ +
+ ) : allDocuments.length === 0 ? ( +
+ +
+ ) : ( + + {fallbackViewMode === 'icons' && ( + + )} + {fallbackViewMode === 'list' && ( + { + setSortBy(b); + setSortDirection(d); + }} + collapsedFolders={collapsedFolders} + onToggleCollapse={toggleFolderCollapse} + onDeleteFolder={handleDeleteFolder} + onDeleteDoc={handleDeleteDoc} + onDropOnFolder={handleDropOnFolder} + onMergeIntoFolder={handleMergeIntoFolder} + /> + )} + {fallbackViewMode === 'columns' && ( + + )} + {fallbackViewMode === 'gallery' && ( + + )} + + )} = { + sm: 'grid-cols-3 sm:grid-cols-5 md:grid-cols-7 lg:grid-cols-8', + md: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6', + lg: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5', + xl: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4', +}; +function IconsSkeleton({ iconSize }: { iconSize: IconSize }) { return ( -
-
-
-
-
-
-
- -
- {placeholders.map((_, index) => ( -
- {viewMode === 'grid' ? ( - <> -
-
-
-
-
- - ) : null} +
+
+ {Array.from({ length: 12 }).map((_, index) => ( +
+
+
+
+
+
))}
); } + +function ListSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 10 }).map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+
+ ); +} + +function ColumnsSkeleton() { + return ( +
+
+
+
+
+
+ {Array.from({ length: 11 }).map((_, rowIndex) => ( +
+ ))} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +function GallerySkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 10 }).map((_, index) => ( +
+
+
+
+
+
+ ))} +
+
+
+ ); +} + +export function DocumentListSkeleton({ + viewMode = 'icons', + iconSize = 'md', +}: DocumentListSkeletonProps) { + let body; + if (viewMode === 'list') { + body = ; + } else if (viewMode === 'columns') { + body = ; + } else if (viewMode === 'gallery') { + body = ; + } else { + body = ; + } + + return ( +
+ {body} +
+ ); +} diff --git a/src/components/doclist/views/ColumnsView.tsx b/src/components/doclist/views/ColumnsView.tsx index 507278d..67b87ce 100644 --- a/src/components/doclist/views/ColumnsView.tsx +++ b/src/components/doclist/views/ColumnsView.tsx @@ -1,7 +1,7 @@ 'use client'; import Link from 'next/link'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import type { DocumentListDocument, Folder } from '@/types/documents'; import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons'; @@ -74,15 +74,17 @@ function ColumnDocRow({ data-doc-tile onClick={onClick} className={ - 'flex items-center gap-2 px-2 py-1.5 rounded-md text-[12px] transition-colors duration-200 ease-out cursor-pointer ' + + 'group flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border transform transition-all duration-200 ease-out cursor-pointer text-left hover:scale-[1.01] ' + (active || isSelected - ? 'bg-accent text-background' - : 'text-foreground hover:bg-offbase') + + ? 'border-accent bg-offbase text-accent' + : 'border-transparent text-foreground hover:border-accent hover:bg-offbase hover:text-accent') + (isOver && canDrop ? ' ring-1 ring-accent' : '') + (isDragging ? ' opacity-50' : '') } > - + + + {doc.name}
); @@ -111,19 +113,19 @@ function ColumnFolderRow({ ref={dropRef as unknown as React.RefObject} onClick={onClick} className={ - 'flex items-center gap-2 px-2 py-1.5 rounded-md text-[12px] transition-colors duration-200 ease-out cursor-pointer ' + + 'group flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border transform transition-all duration-200 ease-out cursor-pointer text-left hover:scale-[1.01] ' + (active - ? 'bg-accent text-background' - : 'text-foreground hover:bg-offbase') + + ? 'border-accent bg-offbase text-accent' + : 'border-transparent text-foreground hover:border-accent hover:bg-offbase hover:text-accent') + (isOver && canDrop ? ' ring-1 ring-accent' : '') } > - + {folder.name} - + {folder.documents.length} - +
); } @@ -150,19 +152,41 @@ export function ColumnsView({ }: ColumnsViewProps) { const { setVisibleOrder } = useDocumentSelection(); const [selected, setSelected] = useState(null); + const allDocs = useMemo( + () => [...folders.flatMap((f) => f.documents), ...unfolderedDocs], + [folders, unfolderedDocs], + ); useEffect(() => { - const all = [...folders.flatMap((f) => f.documents), ...unfolderedDocs]; - setVisibleOrder(all); - }, [folders, unfolderedDocs, setVisibleOrder]); + setVisibleOrder(allDocs); + }, [allDocs, setVisibleOrder]); - // Drop selection if it no longer exists. + // Keep selection valid and default to the first document when entering this view. useEffect(() => { - if (!selected) return; - if (selected.kind === 'folder' && !folders.find((f) => f.id === selected.id)) { - setSelected(null); + if (allDocs.length === 0) { + if (selected !== null) setSelected(null); + return; } - }, [folders, selected]); + + if (!selected) { + setSelected({ kind: 'doc', doc: allDocs[0] }); + return; + } + + if (selected.kind === 'folder') { + if (!folders.find((f) => f.id === selected.id)) { + setSelected({ kind: 'doc', doc: allDocs[0] }); + } + return; + } + + const selectedStillExists = allDocs.some( + (d) => d.id === selected.doc.id && d.type === selected.doc.type, + ); + if (!selectedStillExists) { + setSelected({ kind: 'doc', doc: allDocs[0] }); + } + }, [allDocs, folders, selected]); const selectedFolder = selected?.kind === 'folder' ? folders.find((f) => f.id === selected.id) : undefined;