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:
parent
c264fd551d
commit
6ce4e5a877
|
|
@ -82,15 +82,75 @@
|
|||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-muted/50;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-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 {
|
||||
@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 */
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default function RootLayout({
|
|||
>
|
||||
<div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
<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}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -185,22 +185,8 @@ export default function VirtualizedMediaGrid({
|
|||
fetchItems(0, searchTerm);
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
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]);
|
||||
// Disabled automatic loading to prevent premature batch loading
|
||||
// Users will manually click "Load More" button when they want more content
|
||||
|
||||
const Cell = ({ columnIndex, rowIndex, style }: any) => {
|
||||
const columnCount = getColumnCount();
|
||||
|
|
@ -283,20 +269,31 @@ export default function VirtualizedMediaGrid({
|
|||
const columnWidth = getColumnWidth();
|
||||
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) {
|
||||
return (
|
||||
<div className="min-h-screen p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<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">
|
||||
{type === 'video' ?
|
||||
<Film 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 className="h-screen flex flex-col overflow-hidden">
|
||||
<div className="flex-1 flex items-center justify-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">
|
||||
{type === 'video' ?
|
||||
<Film 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>
|
||||
</div>
|
||||
|
|
@ -304,10 +301,10 @@ export default function VirtualizedMediaGrid({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-6">
|
||||
<div ref={containerRef} className="max-w-7xl mx-auto">
|
||||
<div className="h-screen flex flex-col overflow-hidden">
|
||||
<div ref={containerRef} className="flex-1 flex flex-col max-w-7xl mx-auto w-full overflow-hidden">
|
||||
{/* 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={`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' :
|
||||
|
|
@ -343,48 +340,69 @@ export default function VirtualizedMediaGrid({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Media Grid */}
|
||||
{items.length > 0 && containerWidth > 0 ? (
|
||||
<div className="w-full">
|
||||
<FixedSizeGrid
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
height={Math.min(window.innerHeight - 200, rowCount * 300)}
|
||||
rowCount={rowCount}
|
||||
rowHeight={300}
|
||||
width={containerWidth}
|
||||
itemData={items}
|
||||
>
|
||||
{Cell}
|
||||
</FixedSizeGrid>
|
||||
|
||||
{pagination.hasMore && (
|
||||
<div ref={observerTarget} className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
{/* Media Grid Container */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{items.length > 0 && containerWidth > 0 ? (
|
||||
<div className="h-full relative overflow-hidden">
|
||||
<FixedSizeGrid
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
height={getAvailableHeight()}
|
||||
rowCount={rowCount}
|
||||
rowHeight={300}
|
||||
width={containerWidth}
|
||||
itemData={items}
|
||||
className="custom-scrollbar"
|
||||
>
|
||||
{Cell}
|
||||
</FixedSizeGrid>
|
||||
|
||||
{/* Fancy scroll indicator */}
|
||||
<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="w-full h-full bg-gradient-to-b from-primary/60 via-primary to-primary/60 rounded-full scroll-indicator"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="max-w-sm mx-auto">
|
||||
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<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>
|
||||
|
||||
{pagination.hasMore && (
|
||||
<div className="flex justify-center py-8">
|
||||
{isLoadingMore ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||
<span className="text-sm text-muted-foreground">Loading more {type === 'bookmark' ? 'items' : `${type}s`}...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={loadMoreItems}
|
||||
disabled={loadingRef.current}
|
||||
className="px-6 py-2"
|
||||
>
|
||||
Load More {type === 'bookmark' ? 'Items' : `${type}s`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
)}
|
||||
) : items.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue