Refactor movie filtering and management features: update MovieList and MovieCard components to use new filter props; enhance global store with updateMovie functionality; implement movie state updates for seen and favorite statuses.

This commit is contained in:
Norbert Maciaszek 2025-08-05 22:30:53 +02:00
parent b08cdea130
commit 3809110a39
5 changed files with 93 additions and 31 deletions

View File

@ -7,13 +7,18 @@ export default async function Home() {
<SearchMovies /> <SearchMovies />
<MovieList <MovieList
heading="Upcoming" heading="Upcoming"
onlyUpcoming filterUpcoming={1}
sort="releaseDate" sort="releaseDate"
sortDirection="desc" sortDirection="desc"
/> />
<MovieList heading="My Watchlist" onlyReleased showFilters /> <MovieList
<MovieList heading="Seen" onlySeen /> heading="My Watchlist"
<MovieList heading="Favorites" onlyFavorites /> filterReleased={1}
filterSeen={0}
showFilters
/>
<MovieList heading="Seen" filterSeen={1} />
<MovieList heading="Favorites" filterFavorites={1} />
</main> </main>
); );
} }

View File

@ -2,16 +2,20 @@
import { movies } from "@/lib/db/schema"; import { movies } from "@/lib/db/schema";
import { createContext, FC, use, useState } from "react"; import { createContext, FC, use, useState } from "react";
type Movie = typeof movies.$inferSelect;
type GlobalStore = { type GlobalStore = {
movies: (typeof movies.$inferSelect)[]; movies: Movie[];
addMovie: (movie: typeof movies.$inferSelect) => void; addMovie: (movie: Movie) => void;
deleteMovie: (id: number) => void; deleteMovie: (id: number) => void;
updateMovie: (id: number, movie: Partial<Movie>) => void;
}; };
const globalStore = createContext<GlobalStore>({ const globalStore = createContext<GlobalStore>({
movies: [], movies: [],
addMovie: () => {}, addMovie: () => {},
deleteMovie: () => {}, deleteMovie: () => {},
updateMovie: () => {},
}); });
type Props = { type Props = {
@ -22,7 +26,7 @@ type Props = {
export const GlobalStoreProvider: FC<Props> = ({ children, initialMovies }) => { export const GlobalStoreProvider: FC<Props> = ({ children, initialMovies }) => {
const [movies, setMovies] = useState<GlobalStore["movies"]>(initialMovies); const [movies, setMovies] = useState<GlobalStore["movies"]>(initialMovies);
const addMovie = async (movie: GlobalStore["movies"][number]) => { const addMovie = async (movie: Movie) => {
if (movies.find((m) => m.id === movie.id)) return; if (movies.find((m) => m.id === movie.id)) return;
setMovies((prev) => [...prev, movie]); setMovies((prev) => [...prev, movie]);
@ -32,8 +36,16 @@ export const GlobalStoreProvider: FC<Props> = ({ children, initialMovies }) => {
setMovies((prev) => prev.filter((m) => m.id !== id)); setMovies((prev) => prev.filter((m) => m.id !== id));
}; };
const updateMovie = async (id: number, movie: Partial<Movie>) => {
setMovies((prev) =>
prev.map((m) => (m.id === id ? { ...m, ...movie } : m))
);
};
return ( return (
<globalStore.Provider value={{ movies, addMovie, deleteMovie }}> <globalStore.Provider
value={{ movies, addMovie, deleteMovie, updateMovie }}
>
{children} {children}
</globalStore.Provider> </globalStore.Provider>
); );

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC } from "react";
import { ReadMore } from "../ReadMore"; import { ReadMore } from "../ReadMore";
import { addMovie, deleteMovie } from "@/lib/db"; import { addMovie, deleteMovie, updateMovie } from "@/lib/db";
import { useGlobalStore } from "@/app/store/globalStore"; import { useGlobalStore } from "@/app/store/globalStore";
type Props = { type Props = {
@ -16,6 +16,8 @@ type Props = {
notes?: string; notes?: string;
}; };
const buttonClass = "px-2 py-6 text-sm w-full transition-colors cursor-pointer";
export const MovieCard: FC<Props> = ({ export const MovieCard: FC<Props> = ({
id, id,
title, title,
@ -31,9 +33,12 @@ export const MovieCard: FC<Props> = ({
movies, movies,
addMovie: addMovieToStore, addMovie: addMovieToStore,
deleteMovie: deleteMovieFromStore, deleteMovie: deleteMovieFromStore,
updateMovie: updateMovieInStore,
} = useGlobalStore(); } = useGlobalStore();
const alreadyInStore = movies.find((m) => m.id === id); const alreadyInStore = movies.find((m) => m.id === id);
const isReleased = new Date(releaseDate) < new Date();
const handleAdd = async () => { const handleAdd = async () => {
const movie = { const movie = {
id, id,
@ -55,6 +60,16 @@ export const MovieCard: FC<Props> = ({
deleteMovieFromStore(id); deleteMovieFromStore(id);
}; };
const handleSeen = async () => {
await updateMovie(id, { seen: seen ? 0 : 1 });
updateMovieInStore(id, { seen: seen ? 0 : 1 });
};
const handleFavorite = async () => {
await updateMovie(id, { favorite: favorite ? 0 : 1 });
updateMovieInStore(id, { favorite: favorite ? 0 : 1 });
};
return ( return (
<div className="flex w-full shadow-md rounded-lg overflow-hidden mx-auto group/card"> <div className="flex w-full shadow-md rounded-lg overflow-hidden mx-auto group/card">
<div className="overflow-hidden rounded-xl relative movie-item text-white movie-card"> <div className="overflow-hidden rounded-xl relative movie-item text-white movie-card">
@ -85,22 +100,41 @@ export const MovieCard: FC<Props> = ({
</div> </div>
</div> </div>
</div> </div>
<div className="absolute top-0 z-10 bg-transparent inset-x-0 group-hover/card:bg-white/50 transition-all opacity-0 group-hover/card:opacity-100 flex justify-center"> {/* <div className="absolute top-0 z-10 bg-transparent inset-x-0 group-hover/card:bg-white/50 transition-all opacity-0 group-hover/card:opacity-100 flex flex-col justify-center"> */}
<div className="absolute top-0 z-10 bg-transparent inset-0 group-hover/card:bg-black/50 transition-all opacity-0 group-hover/card:opacity-100 flex flex-col justify-center">
{!alreadyInStore && ( {!alreadyInStore && (
<button <button
className="bg-primary/70 text-white px-4 py-2 rounded-md w-full hover:bg-primary transition-colors cursor-pointer" className={`${buttonClass} bg-primary/70 text-white hover:bg-primary`}
onClick={handleAdd} onClick={handleAdd}
> >
Add to watchlist Add to watchlist
</button> </button>
)} )}
{alreadyInStore && ( {alreadyInStore && (
<button <>
className="bg-primary/70 text-white px-4 py-2 rounded-md w-full hover:bg-primary transition-colors cursor-pointer" {isReleased && (
onClick={handleRemove} <button
> className={`${buttonClass} bg-accent/70 text-white hover:bg-accent`}
Remove from watchlist onClick={handleSeen}
</button> >
{seen ? "Mark as unseen" : "Mark as seen"}
</button>
)}
<button
className={`${buttonClass} bg-amber-400/70 text-white hover:bg-amber-500`}
onClick={handleFavorite}
>
{favorite ? "Remove favorite" : "Add to favorites"}
</button>
<button
className={`${buttonClass} bg-primary/70 text-white hover:bg-primary`}
onClick={handleRemove}
>
Remove from watchlist
</button>
</>
)} )}
</div> </div>
<figure className="absolute inset-0 w-full bottom-[20%]"> <figure className="absolute inset-0 w-full bottom-[20%]">

View File

@ -5,10 +5,10 @@ import { useGlobalStore } from "@/app/store/globalStore";
type Props = { type Props = {
heading: string; heading: string;
onlySeen?: boolean; filterSeen?: 0 | 1;
onlyFavorites?: boolean; filterFavorites?: 0 | 1;
onlyUpcoming?: boolean; filterUpcoming?: 0 | 1;
onlyReleased?: boolean; filterReleased?: 0 | 1;
showFilters?: boolean; showFilters?: boolean;
sort?: "title" | "releaseDate" | "popularity"; sort?: "title" | "releaseDate" | "popularity";
@ -17,10 +17,10 @@ type Props = {
export const MovieList: FC<Props> = ({ export const MovieList: FC<Props> = ({
heading, heading,
onlyFavorites, filterSeen,
onlySeen, filterFavorites,
onlyUpcoming, filterUpcoming,
onlyReleased, filterReleased,
showFilters = false, showFilters = false,
sort = "title", sort = "title",
sortDirection = "asc", sortDirection = "asc",
@ -31,11 +31,15 @@ export const MovieList: FC<Props> = ({
const { movies } = useGlobalStore(); const { movies } = useGlobalStore();
const filteredMovies = movies.filter((movie) => { const filteredMovies = movies.filter((movie) => {
if (onlySeen) return movie.seen === 1; let result = true;
if (onlyFavorites) return movie.favorite === 1; if (typeof filterSeen === "number") result = movie.seen === filterSeen;
if (onlyUpcoming) return new Date(movie.releaseDate) > new Date(); if (typeof filterFavorites === "number")
if (onlyReleased) return new Date(movie.releaseDate) < new Date(); result = result && movie.favorite === filterFavorites;
return true; if (typeof filterUpcoming === "number")
result = result && new Date(movie.releaseDate) > new Date();
if (typeof filterReleased === "number")
result = result && new Date(movie.releaseDate) < new Date();
return result;
}); });
let sortedMovies = filteredMovies.sort((a, b) => { let sortedMovies = filteredMovies.sort((a, b) => {

View File

@ -16,3 +16,10 @@ export const addMovie = async (movie: typeof movies.$inferInsert) => {
export const deleteMovie = async (id: number) => { export const deleteMovie = async (id: number) => {
await db.delete(movies).where(eq(movies.id, id)); await db.delete(movies).where(eq(movies.id, id));
}; };
export const updateMovie = async (
id: number,
movie: Partial<typeof movies.$inferInsert>
) => {
await db.update(movies).set(movie).where(eq(movies.id, id));
};