Refactor MovieCard and MovieList components: streamline props by integrating Movie type, enhance filtering logic, and improve UI responsiveness with a new Dropdown for sorting options.

This commit is contained in:
Norbert Maciaszek 2025-08-11 23:52:29 +02:00
parent 556bb38589
commit 186e98262b
3 changed files with 89 additions and 117 deletions

View File

@ -3,20 +3,10 @@ import { MovieList } from "@/components/molecules/MovieList";
export default async function Home() { export default async function Home() {
return ( return (
<> <>
<MovieList <MovieList heading="Upcoming" filterUpcoming sortDirection="desc" />
heading="Upcoming" <MovieList heading="My Watchlist" filterReleased />
filterUpcoming={1} <MovieList heading="Seen" filterSeen />
sort="releaseDate" <MovieList heading="Favorites" filterFavorites />
sortDirection="desc"
/>
<MovieList
heading="My Watchlist"
filterReleased={1}
filterSeen={0}
showFilters
/>
<MovieList heading="Seen" filterSeen={1} />
<MovieList heading="Favorites" filterFavorites={1} />
</> </>
); );
} }

View File

@ -6,59 +6,34 @@ import { useGlobalStore } from "@/app/store/globalStore";
import { MdFavorite, MdFavoriteBorder, MdOutlinePostAdd } from "react-icons/md"; import { MdFavorite, MdFavoriteBorder, MdOutlinePostAdd } from "react-icons/md";
import { RxEyeClosed, RxEyeOpen } from "react-icons/rx"; import { RxEyeClosed, RxEyeOpen } from "react-icons/rx";
import { IoMdRemoveCircleOutline } from "react-icons/io"; import { IoMdRemoveCircleOutline } from "react-icons/io";
import { Movie } from "@/types/global";
import { FaFire } from "react-icons/fa";
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
type Props = { type Props = Movie & {
id: number;
title: string;
overview: string;
releaseDate: string;
popularity: number;
imagePath: string;
seen: boolean;
favorite: boolean;
notes?: string;
layout?: "default" | "zeus"; layout?: "default" | "zeus";
}; };
const buttonClass = const buttonClass =
"p-4 text-sm transition-colors cursor-pointer text-center group/toggle"; "p-4 text-sm transition-colors cursor-pointer text-center group/toggle";
export const MovieCard: FC<Props> = ({ export const MovieCard: FC<Props> = ({ layout = "default", ...movie }) => {
id,
title,
releaseDate,
popularity,
overview,
imagePath,
seen,
favorite,
notes,
layout = "default",
}) => {
const { const {
movies, movies,
addMovie: addMovieToStore, addMovie: addMovieToStore,
deleteMovie: deleteMovieFromStore, deleteMovie: deleteMovieFromStore,
updateMovie: updateMovieInStore, updateMovie: updateMovieInStore,
} = useGlobalStore(); } = useGlobalStore();
console.log(movies); const { id, title, overview, popularity, release_date, poster_path } = movie;
const alreadyInStore = movies.find((m) => m.id === id); const alreadyInStore = movies.find((m) => m.id === id);
const isReleased = new Date(releaseDate) < new Date(); const isReleased = new Date(release_date) < new Date();
const iconSize = 64; const iconSize = 48;
const seen = movie.seen;
const favorite = movie.favorite;
const handleAdd = async () => { const handleAdd = async () => {
const movie = {
id,
title,
overview,
popularity,
releaseDate,
posterPath: imagePath,
seen: 0,
favorite: 0,
notes: "",
};
await addMovie(movie); await addMovie(movie);
addMovieToStore(movie); addMovieToStore(movie);
}; };
@ -69,13 +44,13 @@ export const MovieCard: FC<Props> = ({
}; };
const handleSeen = async () => { const handleSeen = async () => {
await updateMovie(id, { seen: seen ? 0 : 1 }); await updateMovie(id, { seen: !seen });
updateMovieInStore(id, { seen: seen ? 0 : 1 }); updateMovieInStore(id, { seen: !seen });
}; };
const handleFavorite = async () => { const handleFavorite = async () => {
await updateMovie(id, { favorite: favorite ? 0 : 1 }); await updateMovie(id, { favorite: !favorite });
updateMovieInStore(id, { favorite: favorite ? 0 : 1 }); updateMovieInStore(id, { favorite: !favorite });
}; };
if (layout === "zeus") { if (layout === "zeus") {
@ -83,19 +58,15 @@ export const MovieCard: FC<Props> = ({
<article className="flex flex-col w-full shadow-md rounded-lg overflow-hidden bg-white"> <article className="flex flex-col w-full shadow-md rounded-lg overflow-hidden bg-white">
<figure className="relative "> <figure className="relative ">
<img <img
className="w-full object-cover" className="w-full object-cover xl:h-[420px]"
style={{ height: "420px" }} src={`http://image.tmdb.org/t/p/w342${poster_path}`}
src={`http://image.tmdb.org/t/p/w342/${imagePath}`}
/> />
<span <span className="absolute inset-0 bg-black/30 backdrop-blur-md opacity-0 hover-any:opacity-100 transition-opacity duration-300 flex items-center justify-center cursor-pointer">
className="absolute inset-0 bg-black/30 backdrop-blur-md opacity-0 hover:opacity-100 transition-opacity duration-300 flex items-center justify-center cursor-pointer" {!alreadyInStore && (
onClick={() => { <button className={buttonClass} onClick={handleAdd}>
if (!alreadyInStore) { <MdOutlinePostAdd size={64} color="white" />
handleAdd(); </button>
} )}
}}
>
{!alreadyInStore && <MdOutlinePostAdd size={64} color="white" />}
{alreadyInStore && ( {alreadyInStore && (
<div className="flex flex-col"> <div className="flex flex-col">
<> <>
@ -151,12 +122,25 @@ export const MovieCard: FC<Props> = ({
</div> </div>
)} )}
</span> </span>
<span className="absolute top-0 right-0 bg-black/50 px-2 py-1 rounded-bl-lg">
<p className="text-sm text-white flex items-center gap-1">
<FaFire />
{popularity}
</p>
</span>
</figure> </figure>
<div className="p-4"> <div className="p-4">
<div className="flex justify-between"> <div className="flex justify-between">
<h2 className="text-xl leading-[1.1] font-bold">{title}</h2> <h2 className="text-xl leading-[1.1] font-bold">{title}</h2>
</div> </div>
<p className="text-sm text-gray-500 mt-2">{releaseDate}</p> <p
className={`text-sm mt-2 flex items-center gap-1 leading-[1.1] ${
isReleased ? "text-green-700" : "text-yellow-700"
}`}
>
{isReleased ? <RiCalendarCheckLine /> : <RiCalendarScheduleLine />}
{release_date}
</p>
<div className="text-xs text-gray-400 mt-4"> <div className="text-xs text-gray-400 mt-4">
<ReadMore text={overview} /> <ReadMore text={overview} />
</div> </div>
@ -190,7 +174,7 @@ export const MovieCard: FC<Props> = ({
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm text-gray-400">Release date:</div> <div className="text-sm text-gray-400">Release date:</div>
<div className="release">{releaseDate}</div> <div className="release">{release_date}</div>
</div> </div>
</div> </div>
</div> </div>
@ -235,7 +219,7 @@ export const MovieCard: FC<Props> = ({
<figure className="absolute inset-0 w-full bottom-[20%]"> <figure className="absolute inset-0 w-full bottom-[20%]">
<img <img
className="w-full h-96 object-cover" className="w-full h-96 object-cover"
src={`http://image.tmdb.org/t/p/w342/${imagePath}`} src={`http://image.tmdb.org/t/p/w342${poster_path}`}
/> />
</figure> </figure>
</div> </div>

View File

@ -1,15 +1,21 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { MovieCard } from "@/components/atoms/MovieCard"; import { MovieCard } from "@/components/atoms/MovieCard";
import { Movie } from "@/types/global";
import { useGlobalStore } from "@/app/store/globalStore"; import { useGlobalStore } from "@/app/store/globalStore";
import { Dropdown } from "@/components/atoms/Dropdown";
import { useAutoAnimate } from "@formkit/auto-animate/react";
type Props = { type Props = {
heading: string; heading?: string;
filterSeen?: 0 | 1; overrideMovies?: Movie[];
filterFavorites?: 0 | 1;
filterUpcoming?: 0 | 1;
filterReleased?: 0 | 1;
filterSeen?: boolean;
filterFavorites?: boolean;
filterUpcoming?: boolean;
filterReleased?: boolean;
fluid?: boolean;
showFilters?: boolean; showFilters?: boolean;
sort?: "title" | "releaseDate" | "popularity"; sort?: "title" | "releaseDate" | "popularity";
sortDirection?: "asc" | "desc"; sortDirection?: "asc" | "desc";
@ -17,28 +23,32 @@ type Props = {
export const MovieList: FC<Props> = ({ export const MovieList: FC<Props> = ({
heading, heading,
overrideMovies,
filterSeen, filterSeen,
filterFavorites, filterFavorites,
filterUpcoming, filterUpcoming,
filterReleased, filterReleased,
showFilters = false, fluid = false,
sort = "title", showFilters = true,
sort = "releaseDate",
sortDirection = "asc", sortDirection = "asc",
}) => { }) => {
const { movies: storeMovies } = useGlobalStore();
const [filter, setFilter] = useState<"title" | "releaseDate" | "popularity">( const [filter, setFilter] = useState<"title" | "releaseDate" | "popularity">(
sort sort
); );
const { movies } = useGlobalStore(); const [parent] = useAutoAnimate();
const movies = overrideMovies || storeMovies;
const filteredMovies = movies.filter((movie) => { const filteredMovies = movies.filter((movie) => {
let result = true; let result = true;
if (typeof filterSeen === "number") result = movie.seen === filterSeen; if (filterSeen) result = !!movie.seen;
if (typeof filterFavorites === "number") if (filterFavorites) result = result && !!movie.favorite;
result = result && movie.favorite === filterFavorites; if (filterUpcoming)
if (typeof filterUpcoming === "number") result = result && new Date(movie.release_date) > new Date();
result = result && new Date(movie.releaseDate) > new Date(); if (filterReleased)
if (typeof filterReleased === "number") result = result && new Date(movie.release_date) < new Date();
result = result && new Date(movie.releaseDate) < new Date();
return result; return result;
}); });
@ -46,7 +56,7 @@ export const MovieList: FC<Props> = ({
if (filter === "title") return a.title.localeCompare(b.title); if (filter === "title") return a.title.localeCompare(b.title);
if (filter === "releaseDate") if (filter === "releaseDate")
return ( return (
new Date(b.releaseDate).getTime() - new Date(a.releaseDate).getTime() new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
); );
if (filter === "popularity") return b.popularity - a.popularity; if (filter === "popularity") return b.popularity - a.popularity;
return 0; return 0;
@ -56,32 +66,24 @@ export const MovieList: FC<Props> = ({
sortedMovies = sortedMovies.reverse(); sortedMovies = sortedMovies.reverse();
} }
const handleSort = (sort: "title" | "releaseDate" | "popularity") => {
setFilter(sort);
};
return ( return (
<section className="my-4 md:my-10"> <section className="my-4 md:my-10">
<div className="container"> <div className={`${fluid ? "max-w-full" : "container"}`}>
<div className="row"> <div className="row">
<div className="col-12 md:col-10"> {heading && (
<h2 className="text-2xl font-bold">{heading}</h2> <div className="col-12 md:col-10 flex gap-2 items-center">
</div> {showFilters && (
{showFilters && ( <Dropdown
<div className="col-12 md:col-2"> items={[
<select { label: "Title", value: "title" },
className="bg-accent/70 text-white px-4 py-2 rounded-md w-full hover:bg-primary transition-colors cursor-pointer" { label: "Release Date", value: "releaseDate" },
value={filter} { label: "Popularity", value: "popularity" },
onChange={(e) => ]}
handleSort( defaultValue={filter}
e.target.value as "title" | "releaseDate" | "popularity" callback={(value) => setFilter(value as "title")}
) />
} )}
> <h2 className="text-2xl font-bold">{heading}</h2>
<option value="title">Title</option>
<option value="releaseDate">Release Date</option>
<option value="popularity">Popularity</option>
</select>
</div> </div>
)} )}
</div> </div>
@ -89,16 +91,12 @@ export const MovieList: FC<Props> = ({
<p className="text-text/60 text-sm">No movies found</p> <p className="text-text/60 text-sm">No movies found</p>
)} )}
{filteredMovies.length > 0 && ( {filteredMovies.length > 0 && (
<div className="grid grid-auto-cols-282 gap-6 mt-8 justify-center"> <div
className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-y-6 gap-3 sm:gap-6 mt-8 justify-center"
ref={parent}
>
{sortedMovies.map((movie) => ( {sortedMovies.map((movie) => (
<MovieCard <MovieCard key={movie.id} layout="zeus" {...movie} />
key={movie.id}
layout="zeus"
{...movie}
imagePath={movie.posterPath || ""}
seen={movie.seen === 1}
favorite={movie.favorite === 1}
/>
))} ))}
</div> </div>
)} )}