feat: enhance MovieRow component with drag-and-drop functionality for marking movies as watched or favorite; integrate framer-motion for improved animations and user interaction

This commit is contained in:
Norbert Maciaszek 2025-08-25 22:05:59 +02:00
parent cb0962f184
commit 9079a52778
1 changed files with 126 additions and 49 deletions

View File

@ -1,8 +1,11 @@
"use client";
import { formatter } from "@/helpers/formater";
import { Movie } from "@/types/global";
import Link from "next/link";
import { FC } from "react";
import { FaCalendar, FaClock, FaStar } from "react-icons/fa";
import { FaCalendar, FaClock, FaStar, FaEye, FaHeart } from "react-icons/fa";
import { motion, useAnimationControls, useMotionValue } from "framer-motion";
import { useGlobalStore } from "@/app/store/globalStore";
type Props = {
movie: Movie;
@ -15,6 +18,11 @@ export const MovieRow: FC<Props> = ({
isUpcoming = false,
compact = false,
}) => {
const { movies, addMovie, updateMovie } = useGlobalStore();
const dragControls = useAnimationControls();
const x = useMotionValue(0);
const daysSinceRelease = Math.abs(
Math.floor(
(new Date().getTime() - new Date(movie.release_date).getTime()) /
@ -22,60 +30,129 @@ export const MovieRow: FC<Props> = ({
)
);
// Check if movie is already in store.
const movieInStore = movies.find((m) => m.id === movie.id);
const isWatched = movieInStore?.seen || false;
const isFavorite = movieInStore?.favorite || false;
const handleMarkAsWatched = () => {
if (movieInStore) {
updateMovie(movie.id, { seen: !isWatched });
} else {
addMovie({ ...movie, seen: true, favorite: false });
}
};
const handleAddToFavorites = () => {
if (movieInStore) {
updateMovie(movie.id, { favorite: !isFavorite, seen: true });
} else {
addMovie({ ...movie, seen: true, favorite: true });
}
};
const handleDragAction = () => {
const threshold = 70;
if (x.get() > threshold) {
handleAddToFavorites();
} else if (x.get() < -threshold) {
handleMarkAsWatched();
}
dragControls.start({
x: 0,
});
};
return (
<Link
href={`/film/${movie.id}`}
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800/30 hover:bg-gray-800/50 transition-colors group"
>
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
<img
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
alt={movie.title}
className="object-cover inset-0"
sizes="48px"
/>
<div className="relative overflow-hidden">
{/* Background actions */}
<div className="absolute inset-0 flex">
<div className="absolute right-0 h-full w-24 bg-green-500/20 flex items-center justify-center cursor-pointer">
<FaEye className="w-5 h-5 transition-colors text-green-500" />
</div>
<div className="absolute left-0 h-full w-24 bg-red-500/20 flex items-center justify-center cursor-pointer">
<FaHeart className="w-5 h-5 transition-colors text-red-500" />
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
{movie.title}
</h3>
<div className="flex items-center gap-3 mt-1">
<div className="flex items-center gap-1 text-gray-400 text-xs">
{isUpcoming ? (
<FaCalendar className="w-3 h-3" />
) : (
<FaClock className="w-3 h-3" />
)}
<span>{formatter.formatDate(movie.release_date)}</span>
<motion.div
drag="x"
style={{ x }}
animate={dragControls}
dragConstraints={{ left: -80, right: 80 }}
dragElastic={0.01}
dragMomentum={false}
whileDrag={{ cursor: "grabbing" }}
onDragEnd={handleDragAction}
className="relative z-10"
>
<Link
href={`/film/${movie.id}`}
draggable={false}
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800 hover:bg-gray-800 transition-colors group"
>
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
<img
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
alt={movie.title}
className="object-cover inset-0"
sizes="48px"
/>
</div>
{!!movie.vote_average && (
<div className="flex items-center gap-1 text-yellow-400 text-xs">
<FaStar className="w-3 h-3 fill-current" />
<span>{movie.vote_average.toFixed(1)}</span>
<div className="flex-1 min-w-0">
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
{movie.title}
</h3>
<div className="flex items-center gap-3 mt-1">
<div className="flex items-center gap-1 text-gray-400 text-xs">
{isUpcoming ? (
<FaCalendar className="w-3 h-3" />
) : (
<FaClock className="w-3 h-3" />
)}
<span>{formatter.formatDate(movie.release_date)}</span>
</div>
{!!movie.vote_average && (
<div className="flex items-center gap-1 text-yellow-400 text-xs">
<FaStar className="w-3 h-3 fill-current" />
<span>{movie.vote_average.toFixed(1)}</span>
</div>
)}
{(isFavorite || movie.favorite) && (
<div
className="w-2 h-2 bg-red-500 rounded-full"
title="Ulubione"
/>
)}
{(isWatched || movie.seen) && (
<div
className="w-2 h-2 bg-green-500 rounded-full"
title="Obejrzane"
/>
)}
</div>
</div>
{!compact && (
<div
className={`text-xs px-2 py-1 rounded-full font-medium ${
isUpcoming
? "bg-blue-500/20 text-blue-400"
: "bg-green-500/20 text-green-400"
}`}
>
{isUpcoming
? `za ${daysSinceRelease} dni`
: `od ${daysSinceRelease} dni`}
</div>
)}
{movie.favorite && (
<div className="w-2 h-2 bg-red-500 rounded-full" title="Ulubione" />
)}
</div>
</div>
{!compact && (
<div
className={`text-xs px-2 py-1 rounded-full font-medium ${
isUpcoming
? "bg-blue-500/20 text-blue-400"
: "bg-green-500/20 text-green-400"
}`}
>
{isUpcoming
? `za ${daysSinceRelease} dni`
: `od ${daysSinceRelease} dni`}
</div>
)}
</Link>
</Link>
</motion.div>
</div>
);
};