Refactor MediaCard for internal lazy loading and fix intersection logic

Moved intersection observer logic for lazy loading images from MediaCarousel into MediaCard, allowing each card to handle its own image loading. Simplified MediaCarousel by removing its intersection observer and related loading state, improving component separation and maintainability.
This commit is contained in:
Pas 2025-12-01 16:15:45 -07:00
parent 5e1bd09af5
commit be6aec2c86
2 changed files with 152 additions and 200 deletions

View file

@ -17,23 +17,19 @@ import { MediaBookmarkButton } from "./MediaBookmark";
import { IconPatch } from "../buttons/IconPatch";
import { Icon, Icons } from "../Icon";
// Intersection Observer Hook
// Simple Intersection Observer Hook
function useIntersectionObserver(options: IntersectionObserverInit = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const [hasIntersected, setHasIntersected] = useState(false);
const targetRef = useRef<Element | null>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
if (entry.isIntersecting) {
setHasIntersected(true);
}
},
{
...options,
rootMargin: options.rootMargin || "300px 0px",
rootMargin: options.rootMargin || "300px",
},
);
@ -49,7 +45,7 @@ function useIntersectionObserver(options: IntersectionObserverInit = {}) {
};
}, [options]);
return { targetRef, isIntersecting, hasIntersected };
return { targetRef, isIntersecting };
}
// Skeleton Component
@ -135,8 +131,8 @@ function MediaCardContent({
const [searchQuery] = useSearchQuery();
// Intersection observer for lazy loading
const { targetRef } = useIntersectionObserver({
// Simple intersection observer for lazy loading images
const { targetRef, isIntersecting } = useIntersectionObserver({
rootMargin: "300px",
});
@ -160,6 +156,7 @@ function MediaCardContent({
}
return (
<div ref={targetRef as React.RefObject<HTMLDivElement>}>
<Flare.Base
className={`group -m-[0.705em] rounded-xl bg-background-main transition-colors duration-300 focus:relative focus:z-10 ${
canLink ? "hover:bg-mediaCard-hoverBackground tabbable" : ""
@ -188,9 +185,11 @@ function MediaCardContent({
},
)}
style={{
backgroundImage: media.poster
backgroundImage: isIntersecting
? media.poster
? `url(${media.poster})`
: "url(/placeholder.png)",
: "url(/placeholder.png)"
: "",
}}
>
{series ? (
@ -312,6 +311,7 @@ function MediaCardContent({
)}
</Flare.Child>
</Flare.Base>
</div>
);
}

View file

@ -15,7 +15,6 @@ import {
useDiscoverMedia,
useDiscoverOptions,
} from "@/pages/discover/hooks/useDiscoverMedia";
import { useIntersectionObserver } from "@/pages/discover/hooks/useIntersectionObserver";
import { useDiscoverStore } from "@/stores/discover";
import { useProgressStore } from "@/stores/progress";
import { MediaItem } from "@/utils/mediaTypes";
@ -114,13 +113,6 @@ export function MediaCarousel({
title: item.title || "",
}));
// Set up intersection observer for lazy loading
const { targetRef, isIntersecting, hasIntersected } = useIntersectionObserver(
{
rootMargin: "300px",
},
);
// Handle provider/genre selection
const handleProviderChange = React.useCallback((id: string, name: string) => {
setSelectedProviderId(id);
@ -197,7 +189,7 @@ export function MediaCarousel({
content.type,
]);
// Fetch media using our hook - only when carousel has been visible
// Fetch media using our hook
const { media, sectionTitle, actualContentType } = useDiscoverMedia({
contentType,
mediaType,
@ -207,7 +199,6 @@ export function MediaCarousel({
providerName: selectedProviderName,
mediaTitle: selectedRecommendationTitle,
isCarouselView: true,
enabled: hasIntersected,
});
// Find active button
@ -311,47 +302,8 @@ export function MediaCarousel({
actualContentType,
]);
// Loading state
if (!isIntersecting || !sectionTitle) {
return (
<div ref={targetRef as React.RefObject<HTMLDivElement>}>
<div className="flex items-center justify-between ml-2 md:ml-8 mt-2">
<div className="flex gap-4 items-center">
<h2 className="text-2xl cursor-default font-bold text-white md:text-2xl pl-5 text-balance">
{t("discover.carousel.title.loading")}
</h2>
</div>
</div>
<div className="relative overflow-hidden carousel-container md:pb-4">
<div className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8">
<div className="md:w-12" />
{Array(10)
.fill(null)
.map((_, index) => (
<div
key={`skeleton-loading-${Math.random().toString(36).substring(2)}`}
className="relative mt-4 group cursor-default user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
>
<MediaCard
media={{
id: `skeleton-${index}`,
title: "",
poster: "",
type: isTVShow ? "show" : "movie",
}}
forceSkeleton
/>
</div>
))}
<div className="md:w-12" />
</div>
</div>
</div>
);
}
return (
<div ref={targetRef as React.RefObject<HTMLDivElement>}>
<div>
<div className="flex items-center justify-between ml-2 md:ml-8 mt-2">
<div className="flex flex-col">
<div className="flex items-center gap-4">