feat: enhance scrollbar customization and layout adjustments for media grid

- Updated global CSS to implement custom scrollbar styles, including hover effects and animations.
- Adjusted layout in the media grid component to improve responsiveness and user experience, including dynamic height calculations and loading states.
- Disabled automatic loading of more items, allowing users to manually trigger loading for better control.
This commit is contained in:
tigeren 2025-08-28 15:52:55 +00:00
parent c264fd551d
commit 6ce4e5a877
3 changed files with 152 additions and 74 deletions

View File

@ -82,15 +82,75 @@
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@apply bg-muted/50; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30 rounded-full; background: hsl(var(--muted-foreground) / 0.3);
border-radius: 4px;
transition: background 0.2s ease;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground/50; background: hsl(var(--muted-foreground) / 0.5);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Custom scrollbar for react-window grids */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(180deg,
hsl(var(--primary) / 0.2) 0%,
hsl(var(--primary) / 0.4) 50%,
hsl(var(--primary) / 0.2) 100%);
border-radius: 3px;
transition: all 0.3s ease;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg,
hsl(var(--primary) / 0.4) 0%,
hsl(var(--primary) / 0.6) 50%,
hsl(var(--primary) / 0.4) 100%);
transform: scaleX(1.2);
}
.custom-scrollbar::-webkit-scrollbar-thumb:active {
background: linear-gradient(180deg,
hsl(var(--primary) / 0.6) 0%,
hsl(var(--primary) / 0.8) 50%,
hsl(var(--primary) / 0.6) 100%);
}
/* Hide scrollbar when not needed */
.custom-scrollbar::-webkit-scrollbar-thumb:vertical {
min-height: 40px;
}
/* Fancy scroll indicator animation */
@keyframes scrollPulse {
0%, 100% {
opacity: 0.3;
transform: scaleY(0.8);
}
50% {
opacity: 0.7;
transform: scaleY(1);
}
}
.scroll-indicator {
animation: scrollPulse 2s ease-in-out infinite;
} }
/* Smooth transitions */ /* Smooth transitions */

View File

@ -25,7 +25,7 @@ export default function RootLayout({
> >
<div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20"> <div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20">
<Sidebar /> <Sidebar />
<main className="flex-1 overflow-y-auto bg-background/50 backdrop-blur-sm"> <main className="flex-1 bg-background/50 backdrop-blur-sm">
{children} {children}
</main> </main>
</div> </div>

View File

@ -185,22 +185,8 @@ export default function VirtualizedMediaGrid({
fetchItems(0, searchTerm); fetchItems(0, searchTerm);
}, [type]); }, [type]);
useEffect(() => { // Disabled automatic loading to prevent premature batch loading
const observer = new IntersectionObserver( // Users will manually click "Load More" button when they want more content
entries => {
if (entries[0].isIntersecting && pagination.hasMore && !loadingRef.current) {
loadMoreItems();
}
},
{ threshold: 0.1 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loadMoreItems, pagination.hasMore]);
const Cell = ({ columnIndex, rowIndex, style }: any) => { const Cell = ({ columnIndex, rowIndex, style }: any) => {
const columnCount = getColumnCount(); const columnCount = getColumnCount();
@ -283,20 +269,31 @@ export default function VirtualizedMediaGrid({
const columnWidth = getColumnWidth(); const columnWidth = getColumnWidth();
const rowCount = Math.ceil(items.length / columnCount); const rowCount = Math.ceil(items.length / columnCount);
// Calculate available height for the grid more precisely
const getAvailableHeight = useCallback(() => {
if (typeof window === 'undefined') return 600;
// Calculate the actual header height and other UI elements
const headerHeight = 180; // Title, description, search bar
const bottomPadding = 120; // Load more button area
const availableHeight = window.innerHeight - headerHeight - bottomPadding;
// Ensure minimum height and maximum height
return Math.max(Math.min(availableHeight, window.innerHeight - 100), 400);
}, []);
if (loading && items.length === 0) { if (loading && items.length === 0) {
return ( return (
<div className="min-h-screen p-6"> <div className="h-screen flex flex-col overflow-hidden">
<div className="max-w-7xl mx-auto"> <div className="flex-1 flex items-center justify-center">
<div className="flex items-center justify-center min-h-[60vh]"> <div className="text-center">
<div className="text-center"> <div className="w-16 h-16 bg-gradient-to-br from-primary to-primary/80 rounded-2xl flex items-center justify-center mx-auto mb-4 animate-pulse shadow-lg">
<div className="w-16 h-16 bg-gradient-to-br from-primary to-primary/80 rounded-2xl flex items-center justify-center mx-auto mb-4 animate-pulse shadow-lg"> {type === 'video' ?
{type === 'video' ? <Film className="h-8 w-8 text-primary-foreground" /> :
<Film className="h-8 w-8 text-primary-foreground" /> : <ImageIcon className="h-8 w-8 text-primary-foreground" />
<ImageIcon className="h-8 w-8 text-primary-foreground" /> }
}
</div>
<p className="text-muted-foreground font-medium">Loading {type}s...</p>
</div> </div>
<p className="text-muted-foreground font-medium">Loading {type}s...</p>
</div> </div>
</div> </div>
</div> </div>
@ -304,10 +301,10 @@ export default function VirtualizedMediaGrid({
} }
return ( return (
<div className="min-h-screen p-6"> <div className="h-screen flex flex-col overflow-hidden">
<div ref={containerRef} className="max-w-7xl mx-auto"> <div ref={containerRef} className="flex-1 flex flex-col max-w-7xl mx-auto w-full overflow-hidden">
{/* Header */} {/* Header */}
<div className="mb-6"> <div className="flex-shrink-0 p-6 pb-4">
<div className="flex items-center gap-4 mb-4"> <div className="flex items-center gap-4 mb-4">
<div className={`w-12 h-12 bg-gradient-to-br rounded-xl flex items-center justify-center shadow-lg ${ <div className={`w-12 h-12 bg-gradient-to-br rounded-xl flex items-center justify-center shadow-lg ${
type === 'video' ? 'from-red-500 to-red-600' : type === 'video' ? 'from-red-500 to-red-600' :
@ -343,48 +340,69 @@ export default function VirtualizedMediaGrid({
</div> </div>
</div> </div>
{/* Media Grid */} {/* Media Grid Container */}
{items.length > 0 && containerWidth > 0 ? ( <div className="flex-1 relative overflow-hidden">
<div className="w-full"> {items.length > 0 && containerWidth > 0 ? (
<FixedSizeGrid <div className="h-full relative overflow-hidden">
columnCount={columnCount} <FixedSizeGrid
columnWidth={columnWidth} columnCount={columnCount}
height={Math.min(window.innerHeight - 200, rowCount * 300)} columnWidth={columnWidth}
rowCount={rowCount} height={getAvailableHeight()}
rowHeight={300} rowCount={rowCount}
width={containerWidth} rowHeight={300}
itemData={items} width={containerWidth}
> itemData={items}
{Cell} className="custom-scrollbar"
</FixedSizeGrid> >
{Cell}
</FixedSizeGrid>
{pagination.hasMore && ( {/* Fancy scroll indicator */}
<div ref={observerTarget} className="flex justify-center py-8"> <div className="absolute right-2 top-1/2 transform -translate-y-1/2 w-2 h-32 bg-gradient-to-b from-primary/20 via-primary/40 to-primary/20 rounded-full opacity-0 hover:opacity-100 transition-opacity duration-300 pointer-events-none">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> <div className="w-full h-full bg-gradient-to-b from-primary/60 via-primary to-primary/60 rounded-full scroll-indicator"></div>
</div> </div>
)}
</div> {pagination.hasMore && (
) : items.length === 0 ? ( <div className="flex justify-center py-8">
<div className="text-center py-20"> {isLoadingMore ? (
<div className="max-w-sm mx-auto"> <div className="flex items-center gap-3">
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4"> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<Search className="h-8 w-8 text-muted-foreground" /> <span className="text-sm text-muted-foreground">Loading more {type === 'bookmark' ? 'items' : `${type}s`}...</span>
</div> </div>
<h3 className="text-xl font-semibold text-foreground mb-2"> ) : (
No {type === 'bookmark' ? 'bookmarked items' : `${type}s`} found <Button
</h3> onClick={loadMoreItems}
<p className="text-muted-foreground"> disabled={loadingRef.current}
{searchTerm ? 'Try adjusting your search terms' : className="px-6 py-2"
type === 'bookmark' ? 'Start bookmarking videos and photos to see them here' : >
'Add media libraries and scan for content to get started'} Load More {type === 'bookmark' ? 'Items' : `${type}s`}
</p> </Button>
)}
</div>
)}
</div> </div>
</div> ) : items.length === 0 ? (
) : ( <div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center min-h-[60vh]"> <div className="text-center max-w-sm">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> <div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
</div> <Search className="h-8 w-8 text-muted-foreground" />
)} </div>
<h3 className="text-xl font-semibold text-foreground mb-2">
No {type === 'bookmark' ? 'bookmarked items' : `${type}s`} found
</h3>
<p className="text-muted-foreground">
{searchTerm ? 'Try adjusting your search terms' :
type === 'bookmark' ? 'Start bookmarking videos and photos to see them here' :
'Add media libraries and scan for content to get started'}
</p>
</div>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)}
</div>
</div> </div>
</div> </div>
); );