Skip to content

Commit

Permalink
fix: revert LazyImage component performance issue
Browse files Browse the repository at this point in the history
  • Loading branch information
AlkenD committed Jul 19, 2024
1 parent 55f5721 commit 4fc80dd
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 120 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import MovieInfoPage from "./lib/pages/MovieInfoPage";
import TvInfoPage from "./lib/pages/TvInfoPage";
import getSingleItemData from "./lib/api/getSingleItemData";
import "./index.css";
import ErrorPage from "./lib/pages/ErrorPage";

const App = () => {
const router = createBrowserRouter([
{
path: "/",
element: <Navigation />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
Expand Down
43 changes: 27 additions & 16 deletions src/lib/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import HeroIcon from "./HeroIcon";
import { IconName } from "../types";
import { useNavigate } from "react-router-dom";

const button = cva(
[
Expand Down Expand Up @@ -48,6 +49,7 @@ export interface ButtonProps
VariantProps<typeof button> {
icon?: IconName;
iconType?: "solid" | "outline";
to?: string;
}

const Button: React.FC<ButtonProps> = ({
Expand All @@ -56,23 +58,32 @@ const Button: React.FC<ButtonProps> = ({
size = "small",
icon,
iconType,
to = "",
children,
...props
}) => (
<button className={button({ variant, size, className })} {...props}>
{icon ? (
<div
className={`flex justify-center items-center aspect-square mr-2 ${
size === "large" ? "h-6 w-6" : "h-4 w-4"
}`}
>
<HeroIcon size={size} iconName={icon} type={iconType} />
</div>
) : (
""
)}
<span>{children}</span>
</button>
);
}) => {
const navigate = useNavigate();

return (
<button
className={button({ variant, size, className })}
onClick={() => navigate(to)}
{...props}
>
{icon ? (
<div
className={`flex justify-center items-center aspect-square mr-2 ${
size === "large" ? "h-6 w-6" : "h-4 w-4"
}`}
>
<HeroIcon size={size} iconName={icon} type={iconType} />
</div>
) : (
""
)}
<span>{children}</span>
</button>
);
};

export default Button;
39 changes: 37 additions & 2 deletions src/lib/components/Cards/PersonCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
const PersonCard = ({ personDetails }: any) => {
return <div className="p-2">PersonCard</div>;
import React from "react";

interface PersonDetails {
profile_path?: string;
name: string;
roles: { character: string }[];
department?: string;
}

interface PersonCardProps {
personDetails: PersonDetails;
}

const PersonCard: React.FC<PersonCardProps> = ({ personDetails }) => {
return (
<div className="p-2 w-fit bg-gradient-to-br from-white/20 to-transparent rounded-2xl flex space-x-2 shadow-[inset_0_1px_0_0_#ffffff1a] transition-all bg-size-200 bg-pos-100 hover:bg-pos-0">
<div className="!w-[60px] rounded-lg overflow-hidden aspect-square flex items-center justify-center bg-zinc-600/80">
{personDetails.profile_path ? (
<img
className="w-full h-full object-cover"
src={`https://image.tmdb.org/t/p/w1280${personDetails.profile_path}`}
alt={personDetails.name}
/>
) : (
<div className="text-xs uppercase text-center">Image Not Found</div>
)}
</div>
<div className="px-2 flex-1 whitespace-nowrap">
<div className="font-semibold">{personDetails.name}</div>
<div className="text-xs">
{personDetails.roles?.[0]?.character ||
personDetails.department ||
"N/A"}
</div>
</div>
</div>
);
};

export default PersonCard;
85 changes: 7 additions & 78 deletions src/lib/components/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";

interface ImageProps {
src: string;
Expand All @@ -8,85 +7,15 @@ interface ImageProps {
}

const LazyImage = ({ src, alt, className = "" }: ImageProps) => {
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(() => {
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}>
<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>
)}
<motion.img
key={src}
className="w-full h-full object-cover z-0"
src={src}
alt={alt}
/>
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/MainSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "swiper/css/navigation";
import Button from "./Button";
import IconButton from "./IconButton";

const MainSlider = ({ data = [] }: any) => {
const MainSlider = ({ data = [], type = "movie" }: any) => {
// const [images, setImages] = useState<any>({});
useEffect(() => {
// const fetchImages = async () => {
Expand Down Expand Up @@ -55,7 +55,7 @@ const MainSlider = ({ data = [] }: any) => {
<div className="text-white/60">Sci-Fi, Animation</div>
<div className="line-clamp-2 max-w-[600px]">{item.overview}</div>
<div className="flex space-x-4">
<Button icon="PlayIcon" size="large">
<Button icon="PlayIcon" size="large" to={`/${type}/${item.id}`}>
Play Now
</Button>
<Button
Expand Down
2 changes: 1 addition & 1 deletion src/lib/pages/BrowsePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const BrowsePage = () => {

return (
<main className="w-full h-screen overflow-y-auto overscroll-none">
<MainSlider data={movies} />
<MainSlider data={movies} type="movie" />
<section className="space-y-10 py-6 px-16">
<div className="flex">
<ContentSlider
Expand Down
24 changes: 24 additions & 0 deletions src/lib/pages/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useNavigate, useRouteError } from "react-router-dom";

const ErrorPage = () => {
const navigate = useNavigate();
const error: any = useRouteError();

return (
<div className="flex justify-center items-center p-10 h-screen w-full bg-gradient-to-tl from-blue-500/20 via-indigo-500/20 to-pink-500/20">
<div className="p-2 rounded-2xl bg-white/10 text-sm text-center max-w-[600px] space-y-2 shadow-[inset_0_1px_0_0_#ffffff1a]">
<div className="font-semibold text-xl">{error.status}</div>
<div>{error.statusText}</div>
<div className="p-2 bg-white/20 rounded-lg w-full">{error.data}</div>
<button
className="p-2 bg-emerald-500 hover:bg-emerald-400 rounded-lg w-full font-semibold transition-colors"
onClick={() => navigate("/")}
>
RETURN HOME
</button>
</div>
</div>
);
};

export default ErrorPage;
10 changes: 5 additions & 5 deletions src/lib/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import TransitionWrapper from "./TransitionWrapper";
import { useLoaderData } from "react-router-dom";

const HomePage = () => {
const movies = useLoaderData();
const tv = useLoaderData();
return (
<main className="w-full h-screen overflow-y-auto overscroll-none">
<MainSlider data={movies} />
<MainSlider data={tv} type="tv" />
<section className="space-y-10 py-6 px-16">
<div className="flex">
<ContentSlider
title="Weekly Recommendation"
gradiant={true}
type="tv"
data={movies}
data={tv}
/>
</div>
<div className="flex">
<ContentSlider title="Animation" type="tv" data={movies} />
<ContentSlider title="Animation" type="tv" data={tv} />
</div>
<div className="flex">
<ContentSlider title="Animation" type="tv" data={movies} />
<ContentSlider title="Animation" type="tv" data={tv} />
</div>
</section>
</main>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/pages/LibraryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const LibraryPage = () => {

return (
<main className="w-full h-screen overflow-y-auto overscroll-none">
<MainSlider data={movies} />
<MainSlider data={movies} type="tv" />
<section className="space-y-10 py-6 px-16">
<div className="flex">
<ContentSlider
Expand Down
78 changes: 76 additions & 2 deletions src/lib/pages/MovieInfoPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,83 @@
import { useLoaderData } from "react-router-dom";
import TransitionWrapper from "./TransitionWrapper";
import useColorExtractor from "../hooks/useColorExtractor";
import { motion } from "framer-motion";
import { APP_CONFIG, APP_PATHS } from "../../config/config";
import LazyImage from "../components/LazyImage";
import Button from "../components/Button";
import IconButton from "../components/IconButton";
import PersonCard from "../components/Cards/PersonCard";

const MovieInfoPage = () => {
const movie = useLoaderData();
return <div>{JSON.stringify(movie)}</div>;
const movie: any = useLoaderData();

const { colors } = useColorExtractor(
`${APP_PATHS.image}${movie.backdrop_path}`,
APP_CONFIG.optimization
);

const bgColor = colors.length > 0 ? colors[0] : "#0E0E0E";

return (
<motion.div
animate={{ backgroundColor: bgColor }}
className="w-full h-screen overflow-y-auto overscroll-none pb-12 space-y-12"
>
<div className="relative">
<LazyImage
className="aspect-[21/9]"
src={`${APP_PATHS.image}${movie.backdrop_path}`}
alt="backdrop"
/>
<motion.div
animate={{
background: `linear-gradient(to top, ${bgColor}, transparent)`,
}}
className="absolute top-0 left-0 bottom-0 right-0 w-full h-full"
></motion.div>
<div className="absolute bottom-0 px-[4.5rem] flex items-end space-x-4">
<LazyImage
className="w-[200px] rounded-xl overflow-hidden aspect-[9/12]"
src={`${APP_PATHS.image}${movie.poster_path}`}
alt="poster"
/>
<div className="space-y-4 flex-1">
<div>
<div className="text-4xl font-bold">{movie.title}</div>
</div>
<div className="line-clamp-2 max-w-[700px] text-sm">
{movie.overview}
</div>
<div className="flex space-x-4">
<Button icon="PlayIcon" size="large">
Play Now
</Button>
<IconButton
variant="secondary"
icon="VideoCameraIcon"
iconType="outline"
size="large"
/>
<IconButton
variant="secondary"
icon="BookmarkIcon"
iconType="outline"
size="large"
/>
</div>
</div>
</div>
</div>
<div className="px-[4.5rem] space-y-12">
<div className="text-2xl">Cast</div>
<div className="flex space-x-4 overflow-x-scroll">
{movie.credits.cast.map((cast: any, index: number) => (
<PersonCard key={index} personDetails={cast} />
))}
</div>
</div>
</motion.div>
);
};

export default TransitionWrapper(MovieInfoPage);
Loading

0 comments on commit 4fc80dd

Please sign in to comment.