Skip to content

Commit

Permalink
fix: improve lazy image loading & improved loading state tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
AlkenD committed Jul 19, 2024
1 parent 2205744 commit 55f5721
Show file tree
Hide file tree
Showing 7 changed files with 880 additions and 810 deletions.
43 changes: 7 additions & 36 deletions src/lib/components/Cards/EpisodeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import { AnimatePresence, motion } from "framer-motion";
import { motion } from "framer-motion";
import Chip from "../Chip";
import { useEffect, useState } from "react";
import { APP_PATHS } from "../../../config/config";
import LazyImage from "../LazyImage";

const EpisodeCard = ({ episode, index }: any) => {
const [imageLoading, setImageLoading] = useState(true);

useEffect(() => {
const image = new Image();
image.onload = () => {
setImageLoading(false);
};
image.src = "https://image.tmdb.org/t/p/w1280" + episode.still_path;

return () => {
image.onload = null;
};
}, [episode.still_path]);

return (
<motion.div
className="space-y-2 hover:scale-105 transition-transform"
Expand All @@ -36,26 +23,10 @@ const EpisodeCard = ({ episode, index }: any) => {
S{episode.season_number}:E{episode.episode_number}
</Chip>
</div>
<AnimatePresence>
{!imageLoading ? (
<motion.img
initial={{ opacity: 0 }}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
className="w-full h-full object-cover z-0"
src={"https://image.tmdb.org/t/p/w1280" + episode.still_path}
alt=""
/>
) : (
<motion.div className="text-xs uppercase text-center">
Image Not Found
</motion.div>
)}
</AnimatePresence>
<LazyImage
src={`${APP_PATHS.image}${episode.still_path}`}
alt="episode_thumbnail"
/>
<div className="absolute top-0 left-0 w-full h-full border rounded-2xl border-zinc-400/20 transition-colors"></div>
</div>
<div>
Expand Down
5 changes: 5 additions & 0 deletions src/lib/components/Cards/PersonCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const PersonCard = ({ personDetails }: any) => {

Check failure on line 1 in src/lib/components/Cards/PersonCard.tsx

View workflow job for this annotation

GitHub Actions / publish-tauri (macos-latest, --target aarch64-apple-darwin)

'personDetails' is declared but its value is never read.

Check failure on line 1 in src/lib/components/Cards/PersonCard.tsx

View workflow job for this annotation

GitHub Actions / publish-tauri (macos-latest, --target x86_64-apple-darwin)

'personDetails' is declared but its value is never read.

Check failure on line 1 in src/lib/components/Cards/PersonCard.tsx

View workflow job for this annotation

GitHub Actions / publish-tauri (ubuntu-22.04)

'personDetails' is declared but its value is never read.

Check failure on line 1 in src/lib/components/Cards/PersonCard.tsx

View workflow job for this annotation

GitHub Actions / publish-tauri (windows-latest)

'personDetails' is declared but its value is never read.
return <div className="p-2">PersonCard</div>;
};

export default PersonCard;
58 changes: 42 additions & 16 deletions src/lib/components/EpisodeGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ interface EpisodeGridProps {
const EpisodeGrid = ({ currentSeason, tvDetails }: EpisodeGridProps) => {
const [seasonDetails, setSeasonDetails] = useState<Season | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(true);

const episodesPerPage = 8;

const getSeasonDetails = async () => {
setIsLoading(true);
try {
const res = await fetch(
`${APP_PATHS.tv}/${tvDetails.id}/season/${currentSeason}`,
Expand All @@ -49,16 +51,23 @@ const EpisodeGrid = ({ currentSeason, tvDetails }: EpisodeGridProps) => {
}
const data = await res.json();
setSeasonDetails(data);
setIsLoading(false);
} catch (error) {
console.error("Error fetching season details:", error);
setIsLoading(false);
}
};

useEffect(() => {
getSeasonDetails();
setCurrentPage(1);
}, [currentSeason]);

useEffect(() => {
if (currentPage === 1) {
getSeasonDetails();
}
}, [currentPage, currentSeason]);

if (!seasonDetails) {
return <div>Loading...</div>;
}
Expand Down Expand Up @@ -106,7 +115,7 @@ const EpisodeGrid = ({ currentSeason, tvDetails }: EpisodeGridProps) => {
);

return (
<>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-2xl font-semibold flex items-center space-x-2">
<div>Episodes</div>
Expand Down Expand Up @@ -139,22 +148,39 @@ const EpisodeGrid = ({ currentSeason, tvDetails }: EpisodeGridProps) => {
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key="episodeList"
className="grid grid-cols-3 xl:grid-cols-4 gap-5"
>
{seasonDetails.episodes
.slice(startEpisode - 1, endEpisode)
.map((episode, index) => (
<EpisodeCard
key={episode.id || index}
episode={episode}
index={index}
/>
{isLoading ? (
<motion.div
key="loadingSkeliton"
initial={{ opacity: 0.5 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0.5 }}
className="grid grid-cols-3 xl:grid-cols-4 gap-6"
>
{[1, 2, 3, 4, 5, 6, 7, 8].map((index) => (
<div key={index} className="w-full space-y-2">
<div className="aspect-video bg-zinc-700 rounded-2xl relative overflow-hidden animate-pulse"></div>
<div className="h-5 rounded-full bg-zinc-700 w-48 animate-pulse"></div>
</div>
))}
</motion.div>
</motion.div>
) : (
<motion.div
key="episodeList"
className="grid grid-cols-3 xl:grid-cols-4 gap-5"
>
{seasonDetails.episodes
.slice(startEpisode - 1, endEpisode)
.map((episode, index) => (
<EpisodeCard
key={episode.id || index}
episode={episode}
index={index}
/>
))}
</motion.div>
)}
</AnimatePresence>
</>
</div>
);
};

Expand Down
93 changes: 76 additions & 17 deletions src/lib/components/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";

interface ImageProps {
src: string;
Expand All @@ -8,27 +8,86 @@ interface ImageProps {
}

const LazyImage = ({ src, alt, className = "" }: ImageProps) => {
const [imageLoading, setImageLoading] = useState(true);
const imgEl = useRef<HTMLImageElement>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
const isImageValid = !src.includes("imagenull");

const onImageLoaded = () => setIsLoaded(true);
const onImageError = () => setHasError(true);

useEffect(() => {
const imgElCurrent = imgEl.current;

if (imgElCurrent) {
imgElCurrent.addEventListener("load", onImageLoaded);
imgElCurrent.addEventListener("error", onImageError);
return () => {
imgElCurrent.removeEventListener("load", onImageLoaded);
imgElCurrent.removeEventListener("error", onImageError);
};
}
}, [imgEl]);

useEffect(() => {
setImageLoading(true);
if (!isImageValid) {
setHasError(true);
return;
}

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && imgEl.current) {
imgEl.current.src = src;
observer.disconnect();
}
});
},
{ threshold: 0.1 }
);

if (imgEl.current) {
observer.observe(imgEl.current);
}

return () => {
if (imgEl.current) {
observer.unobserve(imgEl.current);
}
};
}, [src]);

return (
<div className={className}>
<motion.img
variants={{
show: { opacity: 1 },
hide: { opacity: 0 },
}}
initial="hide"
animate={imageLoading ? "hide" : "show"}
transition={{ duration: 0.4 }}
className="w-full h-full object-cover z-0"
onLoad={() => setImageLoading(false)}
src={src}
alt={alt}
/>
<div className="w-full h-full relative">
<AnimatePresence>
{!isLoaded && !hasError && (
<motion.div
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="animate-pulse bg-zinc-700 absolute top-0 left-0 w-full h-full"
></motion.div>
)}
{!hasError && (
<motion.img
ref={imgEl}
key={src}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full object-cover z-0"
src={src}
alt={alt}
/>
)}
</AnimatePresence>
{(hasError || !isImageValid) && (
<div className="flex justify-center items-center absolute top-0 left-0 w-full h-full text-center text-xs">
IMAGE NOT FOUND
</div>
)}
</div>
</div>
);
};
Expand Down
9 changes: 9 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export default {
"pos-0": "0% 0%",
"pos-100": "80% 80%",
},
keyframes: {
pulseBg: {
"0%, 100%": { backgroundPosition: "200% 0" },
"50%": { backgroundPosition: "-200% 0" },
},
},
animation: {
pulseBg: "pulseBg 1.5s ease-in-out infinite",
},
},
},
plugins: [],
Expand Down
Loading

0 comments on commit 55f5721

Please sign in to comment.