mirror of
https://github.com/sussy-code/smov.git
synced 2026-05-18 23:51:43 +00:00
Add lazy loading to discover
This commit is contained in:
parent
7a93410530
commit
d58e32b1d9
6 changed files with 332 additions and 101 deletions
|
|
@ -1,8 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Loading } from "@/components/layout/Loading";
|
|
||||||
import DiscoverContent from "@/pages/discover/discoverContent";
|
import DiscoverContent from "@/pages/discover/discoverContent";
|
||||||
|
|
||||||
import { SubPageLayout } from "../layouts/SubPageLayout";
|
import { SubPageLayout } from "../layouts/SubPageLayout";
|
||||||
|
|
@ -11,25 +9,6 @@ import { PageTitle } from "../parts/util/PageTitle";
|
||||||
export function Discover() {
|
export function Discover() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Stupid method to "simulate" loading so the user understands this takes a white to load.
|
|
||||||
// TO DO: Lazy load all the media cards 💀
|
|
||||||
|
|
||||||
// State to track whether content is loading or loaded
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Simulate loading media cards
|
|
||||||
useEffect(() => {
|
|
||||||
const simulateLoading = async () => {
|
|
||||||
// Simulate a loading time with setTimeout or fetch data here
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, 2000);
|
|
||||||
}); // Simulate 2s loading time
|
|
||||||
setLoading(false); // After loading, set loading to false
|
|
||||||
};
|
|
||||||
|
|
||||||
simulateLoading();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -64,20 +43,7 @@ export function Discover() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conditional rendering: show loading screen or the content */}
|
<DiscoverContent />
|
||||||
{loading ? (
|
|
||||||
<div className="flex flex-col justify-center items-center h-64 space-y-4">
|
|
||||||
<Loading />
|
|
||||||
<p className="text-lg font-medium text-gray-400 animate-pulse mt-4">
|
|
||||||
Fetching the latest movies & TV shows...
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Please wait while we load the best recommendations for you.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<DiscoverContent />
|
|
||||||
)}
|
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
src/pages/discover/components/LazyMediaCarousel.tsx
Normal file
78
src/pages/discover/components/LazyMediaCarousel.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Category, Genre, Media } from "@/pages/discover/common";
|
||||||
|
import { useIntersectionObserver } from "@/pages/discover/hooks/useIntersectionObserver";
|
||||||
|
import { useLazyTMDBData } from "@/pages/discover/hooks/useTMDBData";
|
||||||
|
|
||||||
|
import { MediaCarousel } from "./MediaCarousel";
|
||||||
|
|
||||||
|
interface LazyMediaCarouselProps {
|
||||||
|
category?: Category | null;
|
||||||
|
genre?: Genre | null;
|
||||||
|
mediaType: "movie" | "tv";
|
||||||
|
movieWidth: string;
|
||||||
|
isMobile: boolean;
|
||||||
|
carouselRefs: React.MutableRefObject<{
|
||||||
|
[key: string]: HTMLDivElement | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LazyMediaCarousel({
|
||||||
|
category,
|
||||||
|
genre,
|
||||||
|
mediaType,
|
||||||
|
movieWidth,
|
||||||
|
isMobile,
|
||||||
|
carouselRefs,
|
||||||
|
}: LazyMediaCarouselProps) {
|
||||||
|
const [medias, setMedias] = useState<Media[]>([]);
|
||||||
|
|
||||||
|
// Use intersection observer to detect when this component is visible
|
||||||
|
const { targetRef, isIntersecting, hasIntersected } = useIntersectionObserver(
|
||||||
|
{ rootMargin: "200px" }, // Load when within 200px of viewport
|
||||||
|
true, // Only trigger once
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the lazy loading hook to fetch data when visible
|
||||||
|
const { media, isLoading } = useLazyTMDBData(
|
||||||
|
genre || null,
|
||||||
|
category || null,
|
||||||
|
mediaType,
|
||||||
|
hasIntersected,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update medias when data is loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (media.length > 0) {
|
||||||
|
setMedias(media);
|
||||||
|
}
|
||||||
|
}, [media]);
|
||||||
|
|
||||||
|
const categoryName = category?.name || genre?.name || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={targetRef as React.RefObject<HTMLDivElement>}>
|
||||||
|
{isIntersecting ? (
|
||||||
|
<MediaCarousel
|
||||||
|
medias={medias}
|
||||||
|
category={categoryName}
|
||||||
|
isTVShow={mediaType === "tv"}
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative overflow-hidden carousel-container">
|
||||||
|
<h2 className="ml-2 md:ml-8 mt-2 text-2xl cursor-default font-bold text-white md:text-2xl mx-auto pl-5 text-balance">
|
||||||
|
{categoryName} {mediaType === "tv" ? "Shows" : "Movies"}
|
||||||
|
</h2>
|
||||||
|
<div className="flex whitespace-nowrap pt-0 pb-4 overflow-auto scrollbar rounded-xl overflow-y-hidden h-[300px] animate-pulse bg-background-secondary/20">
|
||||||
|
<div className="w-full text-center flex items-center justify-center">
|
||||||
|
{isLoading ? "Loading..." : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/pages/discover/components/LazyTabContent.tsx
Normal file
29
src/pages/discover/components/LazyTabContent.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface LazyTabContentProps {
|
||||||
|
isActive: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
preloadWhenInactive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LazyTabContent({
|
||||||
|
isActive,
|
||||||
|
children,
|
||||||
|
preloadWhenInactive = false,
|
||||||
|
}: LazyTabContentProps) {
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load content when tab becomes active or if preload is enabled
|
||||||
|
if (isActive || preloadWhenInactive) {
|
||||||
|
setHasLoaded(true);
|
||||||
|
}
|
||||||
|
}, [isActive, preloadWhenInactive]);
|
||||||
|
|
||||||
|
// Only render children if the tab has been loaded
|
||||||
|
return (
|
||||||
|
<div style={{ display: isActive ? "block" : "none" }}>
|
||||||
|
{hasLoaded ? children : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,8 @@ import { conf } from "@/setup/config";
|
||||||
|
|
||||||
import "./discover.css";
|
import "./discover.css";
|
||||||
import { CategoryButtons } from "./components/CategoryButtons";
|
import { CategoryButtons } from "./components/CategoryButtons";
|
||||||
|
import { LazyMediaCarousel } from "./components/LazyMediaCarousel";
|
||||||
|
import { LazyTabContent } from "./components/LazyTabContent";
|
||||||
import { MediaCarousel } from "./components/MediaCarousel";
|
import { MediaCarousel } from "./components/MediaCarousel";
|
||||||
import { RandomMovieButton } from "./components/RandomMovieButton";
|
import { RandomMovieButton } from "./components/RandomMovieButton";
|
||||||
import { ScrollToTopButton } from "./components/ScrollToTopButton";
|
import { ScrollToTopButton } from "./components/ScrollToTopButton";
|
||||||
|
|
@ -64,13 +66,21 @@ export function DiscoverContent() {
|
||||||
// Hooks
|
// Hooks
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const { genreMedia: genreMovies, categoryMedia: categoryMovies } =
|
const { genreMedia: genreMovies } = useTMDBData(genres, categories, "movie");
|
||||||
useTMDBData(genres, categories, "movie");
|
// const { genreMedia: genreTVShows } = useTMDBData(
|
||||||
const { genreMedia: genreTVShows, categoryMedia: categoryTVShows } =
|
// tvGenres,
|
||||||
useTMDBData(tvGenres, tvCategories, "tv");
|
// tvCategories,
|
||||||
|
// "tv",
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Only load data for the active tab
|
||||||
|
const isMoviesTab = selectedCategory === "movies";
|
||||||
|
const isTVShowsTab = selectedCategory === "tvshows";
|
||||||
|
|
||||||
// Fetch TV show genres
|
// Fetch TV show genres
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isTVShowsTab) return;
|
||||||
|
|
||||||
const fetchTVGenres = async () => {
|
const fetchTVGenres = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await get<any>("/genre/tv/list", {
|
const data = await get<any>("/genre/tv/list", {
|
||||||
|
|
@ -85,10 +95,12 @@ export function DiscoverContent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTVGenres();
|
fetchTVGenres();
|
||||||
}, []);
|
}, [isTVShowsTab]);
|
||||||
|
|
||||||
// Fetch Movie genres
|
// Fetch Movie genres
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isMoviesTab) return;
|
||||||
|
|
||||||
const fetchGenres = async () => {
|
const fetchGenres = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await get<any>("/genre/movie/list", {
|
const data = await get<any>("/genre/movie/list", {
|
||||||
|
|
@ -104,7 +116,7 @@ export function DiscoverContent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchGenres();
|
fetchGenres();
|
||||||
}, []);
|
}, [isMoviesTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
|
@ -182,6 +194,92 @@ export function DiscoverContent() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render Movies content with lazy loading
|
||||||
|
const renderMoviesContent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Provider Movies */}
|
||||||
|
{providerMovies.length > 0 && (
|
||||||
|
<MediaCarousel
|
||||||
|
medias={providerMovies}
|
||||||
|
category={selectedProvider.name}
|
||||||
|
isTVShow={false}
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
{categories.map((category) => (
|
||||||
|
<LazyMediaCarousel
|
||||||
|
key={category.name}
|
||||||
|
category={category}
|
||||||
|
mediaType="movie"
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Genres */}
|
||||||
|
{genres.map((genre) => (
|
||||||
|
<LazyMediaCarousel
|
||||||
|
key={genre.id}
|
||||||
|
genre={genre}
|
||||||
|
mediaType="movie"
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render TV Shows content with lazy loading
|
||||||
|
const renderTVShowsContent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Provider TV Shows */}
|
||||||
|
{providerTVShows.length > 0 && (
|
||||||
|
<MediaCarousel
|
||||||
|
medias={providerTVShows}
|
||||||
|
category={selectedProvider.name}
|
||||||
|
isTVShow
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
{tvCategories.map((category) => (
|
||||||
|
<LazyMediaCarousel
|
||||||
|
key={category.name}
|
||||||
|
category={category}
|
||||||
|
mediaType="tv"
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Genres */}
|
||||||
|
{tvGenres.map((genre) => (
|
||||||
|
<LazyMediaCarousel
|
||||||
|
key={genre.id}
|
||||||
|
genre={genre}
|
||||||
|
mediaType="tv"
|
||||||
|
movieWidth={movieWidth}
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Random Movie Button */}
|
{/* Random Movie Button */}
|
||||||
|
|
@ -247,63 +345,18 @@ export function DiscoverContent() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Content Section */}
|
|
||||||
<div className="w-full md:w-[90%] max-w-[2400px] mx-auto">
|
|
||||||
{(() => {
|
|
||||||
const isMovieCategory = selectedCategory === "movies";
|
|
||||||
const providerMedia = isMovieCategory
|
|
||||||
? providerMovies
|
|
||||||
: providerTVShows;
|
|
||||||
const mediaGenres = isMovieCategory ? genres : tvGenres;
|
|
||||||
const mediaCategories = isMovieCategory ? categories : tvCategories;
|
|
||||||
|
|
||||||
return (
|
{/* Content Section with Lazy Loading Tabs */}
|
||||||
<>
|
<div className="w-full md:w-[90%] max-w-[2400px] mx-auto">
|
||||||
{/* Media Carousels */}
|
{/* Movies Tab */}
|
||||||
{providerMedia.length > 0 && (
|
<LazyTabContent isActive={isMoviesTab}>
|
||||||
<MediaCarousel
|
{renderMoviesContent()}
|
||||||
medias={providerMedia}
|
</LazyTabContent>
|
||||||
category={selectedProvider.name}
|
|
||||||
isTVShow={!isMovieCategory}
|
{/* TV Shows Tab */}
|
||||||
movieWidth={movieWidth}
|
<LazyTabContent isActive={isTVShowsTab}>
|
||||||
isMobile={isMobile}
|
{renderTVShowsContent()}
|
||||||
carouselRefs={carouselRefs}
|
</LazyTabContent>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* Categories and Genres */}
|
|
||||||
{mediaCategories.map((category) => (
|
|
||||||
<MediaCarousel
|
|
||||||
key={category.name}
|
|
||||||
medias={
|
|
||||||
isMovieCategory
|
|
||||||
? categoryMovies[category.name] || []
|
|
||||||
: categoryTVShows[category.name] || []
|
|
||||||
}
|
|
||||||
category={category.name}
|
|
||||||
isTVShow={!isMovieCategory}
|
|
||||||
movieWidth={movieWidth}
|
|
||||||
isMobile={isMobile}
|
|
||||||
carouselRefs={carouselRefs}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{mediaGenres.map((genre) => (
|
|
||||||
<MediaCarousel
|
|
||||||
key={genre.id}
|
|
||||||
medias={
|
|
||||||
isMovieCategory
|
|
||||||
? genreMovies[genre.id] || []
|
|
||||||
: genreTVShows[genre.id] || []
|
|
||||||
}
|
|
||||||
category={genre.name}
|
|
||||||
isTVShow={!isMovieCategory}
|
|
||||||
movieWidth={movieWidth}
|
|
||||||
isMobile={isMobile}
|
|
||||||
carouselRefs={carouselRefs}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollToTopButton />
|
<ScrollToTopButton />
|
||||||
|
|
|
||||||
43
src/pages/discover/hooks/useIntersectionObserver.tsx
Normal file
43
src/pages/discover/hooks/useIntersectionObserver.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface IntersectionObserverOptions {
|
||||||
|
root?: Element | null;
|
||||||
|
rootMargin?: string;
|
||||||
|
threshold?: number | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIntersectionObserver(
|
||||||
|
options: IntersectionObserverOptions = {},
|
||||||
|
onceOnly = false,
|
||||||
|
) {
|
||||||
|
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 && !hasIntersected) {
|
||||||
|
setHasIntersected(true);
|
||||||
|
}
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
const currentTarget = targetRef.current;
|
||||||
|
if (currentTarget) {
|
||||||
|
observer.observe(currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentTarget) {
|
||||||
|
observer.unobserve(currentTarget);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [options, hasIntersected]);
|
||||||
|
|
||||||
|
// If onceOnly is true, we only care if it has ever intersected
|
||||||
|
// Otherwise, we care about the current intersection state
|
||||||
|
const shouldLoad = onceOnly ? hasIntersected : isIntersecting;
|
||||||
|
|
||||||
|
return { targetRef, isIntersecting: shouldLoad, hasIntersected };
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ export function useTMDBData(
|
||||||
genres: Genre[],
|
genres: Genre[],
|
||||||
categories: Category[],
|
categories: Category[],
|
||||||
mediaType: MediaType,
|
mediaType: MediaType,
|
||||||
|
shouldLoad = true,
|
||||||
) {
|
) {
|
||||||
const [genreMedia, setGenreMedia] = useState<{
|
const [genreMedia, setGenreMedia] = useState<{
|
||||||
[id: number]: Movie[] | TVShow[];
|
[id: number]: Movie[] | TVShow[];
|
||||||
|
|
@ -17,13 +18,15 @@ export function useTMDBData(
|
||||||
const [categoryMedia, setCategoryMedia] = useState<{
|
const [categoryMedia, setCategoryMedia] = useState<{
|
||||||
[categoryName: string]: Movie[] | TVShow[];
|
[categoryName: string]: Movie[] | TVShow[];
|
||||||
}>({});
|
}>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// Unified fetch function
|
// Unified fetch function
|
||||||
const fetchMedia = useCallback(
|
const fetchMedia = useCallback(
|
||||||
async (endpoint: string, key: string, isGenre: boolean) => {
|
async (endpoint: string, key: string, isGenre: boolean) => {
|
||||||
try {
|
try {
|
||||||
const media: Movie[] | TVShow[] = [];
|
const media: Movie[] | TVShow[] = [];
|
||||||
for (let page = 1; page <= 6; page += 1) {
|
// Reduce the number of pages to improve performance
|
||||||
|
for (let page = 1; page <= 2; page += 1) {
|
||||||
const data = await get<any>(endpoint, {
|
const data = await get<any>(endpoint, {
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: "en-US",
|
language: "en-US",
|
||||||
|
|
@ -53,7 +56,10 @@ export function useTMDBData(
|
||||||
|
|
||||||
// Fetch media for each genre
|
// Fetch media for each genre
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shouldLoad || genres.length === 0) return;
|
||||||
|
|
||||||
const fetchMediaForGenres = async () => {
|
const fetchMediaForGenres = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
const genrePromises = genres.map(async (genre) => {
|
const genrePromises = genres.map(async (genre) => {
|
||||||
const media = await fetchMedia(
|
const media = await fetchMedia(
|
||||||
`/discover/${mediaType}`,
|
`/discover/${mediaType}`,
|
||||||
|
|
@ -63,23 +69,79 @@ export function useTMDBData(
|
||||||
setGenreMedia((prev) => ({ ...prev, [genre.id]: media }));
|
setGenreMedia((prev) => ({ ...prev, [genre.id]: media }));
|
||||||
});
|
});
|
||||||
await Promise.all(genrePromises);
|
await Promise.all(genrePromises);
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMediaForGenres();
|
fetchMediaForGenres();
|
||||||
}, [genres, mediaType, fetchMedia]);
|
}, [genres, mediaType, fetchMedia, shouldLoad]);
|
||||||
|
|
||||||
// Fetch media for each category
|
// Fetch media for each category
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shouldLoad || categories.length === 0) return;
|
||||||
|
|
||||||
const fetchMediaForCategories = async () => {
|
const fetchMediaForCategories = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
const categoryPromises = categories.map(async (category) => {
|
const categoryPromises = categories.map(async (category) => {
|
||||||
const media = await fetchMedia(category.endpoint, category.name, false);
|
const media = await fetchMedia(category.endpoint, category.name, false);
|
||||||
setCategoryMedia((prev) => ({ ...prev, [category.name]: media }));
|
setCategoryMedia((prev) => ({ ...prev, [category.name]: media }));
|
||||||
});
|
});
|
||||||
await Promise.all(categoryPromises);
|
await Promise.all(categoryPromises);
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMediaForCategories();
|
fetchMediaForCategories();
|
||||||
}, [categories, mediaType, fetchMedia]);
|
}, [categories, mediaType, fetchMedia, shouldLoad]);
|
||||||
|
|
||||||
return { genreMedia, categoryMedia };
|
return { genreMedia, categoryMedia, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a hook for lazy loading a specific genre or category
|
||||||
|
export function useLazyTMDBData(
|
||||||
|
genre: Genre | null,
|
||||||
|
category: Category | null,
|
||||||
|
mediaType: MediaType,
|
||||||
|
shouldLoad = false,
|
||||||
|
) {
|
||||||
|
const [media, setMedia] = useState<Movie[] | TVShow[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchMedia = useCallback(
|
||||||
|
async (endpoint: string, key: string, isGenre: boolean) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const mediaItems: Movie[] | TVShow[] = [];
|
||||||
|
// Only fetch one page for better performance
|
||||||
|
const data = await get<any>(endpoint, {
|
||||||
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
language: "en-US",
|
||||||
|
page: "1",
|
||||||
|
...(isGenre ? { with_genres: key } : {}),
|
||||||
|
});
|
||||||
|
mediaItems.push(...data.results);
|
||||||
|
setMedia(mediaItems);
|
||||||
|
setIsLoading(false);
|
||||||
|
return mediaItems;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error fetching ${mediaType} for ${isGenre ? "genre" : "category"}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mediaType],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldLoad) return;
|
||||||
|
|
||||||
|
if (genre) {
|
||||||
|
fetchMedia(`/discover/${mediaType}`, genre.id.toString(), true);
|
||||||
|
} else if (category) {
|
||||||
|
fetchMedia(category.endpoint, category.name, false);
|
||||||
|
}
|
||||||
|
}, [genre, category, mediaType, fetchMedia, shouldLoad]);
|
||||||
|
|
||||||
|
return { media, isLoading };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue