aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
189 lines (186 loc) • 6.68 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react';
import { useState, useRef, useCallback, useEffect } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
/**
* GlassInfiniteScroll component
* Infinite scrolling container with loading states and error handling
*/
const GlassInfiniteScroll = ({
children,
onLoadMore,
hasMore,
loading = false,
error = null,
threshold = 100,
loadingIndicator,
errorIndicator,
endMessage,
className,
reverse = false,
autoScroll = false,
...props
}) => {
const [isLoading, setIsLoading] = useState(loading);
const [loadError, setLoadError] = useState(error);
const [hasReachedEnd, setHasReachedEnd] = useState(false);
const containerRef = useRef(null);
const sentinelRef = useRef(null);
const observerRef = useRef(null);
const isLoadingMoreRef = useRef(false);
// Handle intersection observer
const handleIntersection = useCallback(async entries => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !isLoadingMoreRef.current && !loadError) {
isLoadingMoreRef.current = true;
setIsLoading(true);
setLoadError(null);
try {
await onLoadMore();
} catch (err) {
console.error("Error loading more items:", err);
setLoadError("Failed to load more items");
} finally {
setIsLoading(false);
isLoadingMoreRef.current = false;
}
}
if (!hasMore && entry.isIntersecting) {
setHasReachedEnd(true);
}
}, [hasMore, onLoadMore, loadError]);
// Setup intersection observer
useEffect(() => {
if (!sentinelRef.current) return;
observerRef.current = new IntersectionObserver(handleIntersection, {
root: containerRef.current,
rootMargin: `${threshold}px`,
threshold: 0.1
});
observerRef.current.observe(sentinelRef.current);
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [handleIntersection, threshold]);
// Update loading state when prop changes
useEffect(() => {
setIsLoading(loading);
}, [loading]);
// Update error state when prop changes
useEffect(() => {
setLoadError(error);
}, [error]);
// Auto-scroll to bottom
useEffect(() => {
if (autoScroll && containerRef.current && reverse) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [children, autoScroll, reverse]);
// Handle retry
const handleRetry = useCallback(async () => {
setLoadError(null);
setIsLoading(true);
try {
await onLoadMore();
} catch (err) {
console.error("Error retrying load:", err);
setLoadError("Failed to load more items");
} finally {
setIsLoading(false);
}
}, [onLoadMore]);
// Default loading indicator
const defaultLoadingIndicator = jsx(MotionFramer, {
preset: "fadeIn",
className: "glass-flex glass-items-center glass-justify-center glass-py-8",
children: jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3 glass-px-4 glass-py-2 glass-surface-subtle/10 glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-radius-full glass-contrast-guard",
children: [jsx(Loader2, {
className: 'w-5 h-5 animate-spin text-primary'
}), jsx("span", {
className: 'text-primary/80 glass-text-sm',
children: "Loading more..."
})]
})
});
// Default error indicator
const defaultErrorIndicator = jsxs(MotionFramer, {
preset: "fadeIn",
className: "glass-flex glass-flex-col glass-items-center glass-justify-center glass-py-8 glass-px-4",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 text-primary mb-3',
children: [jsx(AlertCircle, {
className: 'w-5 h-5'
}), jsx("span", {
className: 'glass-text-sm font-medium',
children: "Error loading more items"
})]
}), jsxs("button", {
onClick: handleRetry,
className: 'glass-flex glass-items-center glass-gap-2 glass-px-4 glass-py-2 glass-surface-dark/30 hover:glass-surface-dark/40 glass-radius-lg transition-colors glass-border glass-border-white/20 hover:border-white/30 glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard',
children: [jsx(RefreshCw, {
className: 'w-4 h-4'
}), jsx("span", {
className: "glass-text-sm",
children: "Try again"
})]
})]
});
// Default end message
const defaultEndMessage = jsx(MotionFramer, {
preset: "fadeIn",
className: 'text-center glass-py-8',
children: jsx("div", {
className: 'text-primary/60 glass-text-sm',
children: "You've reached the end"
})
});
return jsxs("div", {
"data-glass-component": true,
ref: containerRef,
className: cn("overflow-y-auto", reverse && "flex flex-col-reverse", className),
...props,
children: [jsx("div", {
className: cn(reverse && "flex flex-col-reverse"),
children: children
}), !reverse && jsx("div", {
ref: sentinelRef,
className: 'h-4'
}), isLoading && !reverse && jsx("div", {
className: "glass-py-4",
children: loadingIndicator || defaultLoadingIndicator
}), loadError && !reverse && jsx("div", {
className: "glass-py-4",
children: errorIndicator || defaultErrorIndicator
}), hasReachedEnd && !hasMore && !isLoading && !loadError && !reverse && jsx("div", {
className: "glass-py-4",
children: endMessage || defaultEndMessage
}), reverse && jsxs("div", {
className: "glass-flex glass-flex-col-reverse",
children: [jsx("div", {
ref: sentinelRef,
className: 'h-4'
}), isLoading && jsx("div", {
className: "glass-py-4",
children: loadingIndicator || defaultLoadingIndicator
}), loadError && jsx("div", {
className: "glass-py-4",
children: errorIndicator || defaultErrorIndicator
}), hasReachedEnd && !hasMore && !isLoading && !loadError && jsx("div", {
className: "glass-py-4",
children: endMessage || defaultEndMessage
})]
})]
});
};
export { GlassInfiniteScroll, GlassInfiniteScroll as default };
//# sourceMappingURL=GlassInfiniteScroll.js.map