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:
parent
cb0962f184
commit
9079a52778
|
|
@ -1,8 +1,11 @@
|
||||||
|
"use client";
|
||||||
import { formatter } from "@/helpers/formater";
|
import { formatter } from "@/helpers/formater";
|
||||||
import { Movie } from "@/types/global";
|
import { Movie } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FC } from "react";
|
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 = {
|
type Props = {
|
||||||
movie: Movie;
|
movie: Movie;
|
||||||
|
|
@ -15,6 +18,11 @@ export const MovieRow: FC<Props> = ({
|
||||||
isUpcoming = false,
|
isUpcoming = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { movies, addMovie, updateMovie } = useGlobalStore();
|
||||||
|
|
||||||
|
const dragControls = useAnimationControls();
|
||||||
|
const x = useMotionValue(0);
|
||||||
|
|
||||||
const daysSinceRelease = Math.abs(
|
const daysSinceRelease = Math.abs(
|
||||||
Math.floor(
|
Math.floor(
|
||||||
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
(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 (
|
return (
|
||||||
<Link
|
<div className="relative overflow-hidden">
|
||||||
href={`/film/${movie.id}`}
|
{/* Background actions */}
|
||||||
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800/30 hover:bg-gray-800/50 transition-colors group"
|
<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">
|
||||||
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
|
<FaEye className="w-5 h-5 transition-colors text-green-500" />
|
||||||
<img
|
</div>
|
||||||
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
|
|
||||||
alt={movie.title}
|
<div className="absolute left-0 h-full w-24 bg-red-500/20 flex items-center justify-center cursor-pointer">
|
||||||
className="object-cover inset-0"
|
<FaHeart className="w-5 h-5 transition-colors text-red-500" />
|
||||||
sizes="48px"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<motion.div
|
||||||
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
drag="x"
|
||||||
{movie.title}
|
style={{ x }}
|
||||||
</h3>
|
animate={dragControls}
|
||||||
<div className="flex items-center gap-3 mt-1">
|
dragConstraints={{ left: -80, right: 80 }}
|
||||||
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
dragElastic={0.01}
|
||||||
{isUpcoming ? (
|
dragMomentum={false}
|
||||||
<FaCalendar className="w-3 h-3" />
|
whileDrag={{ cursor: "grabbing" }}
|
||||||
) : (
|
onDragEnd={handleDragAction}
|
||||||
<FaClock className="w-3 h-3" />
|
className="relative z-10"
|
||||||
)}
|
>
|
||||||
<span>{formatter.formatDate(movie.release_date)}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
{!!movie.vote_average && (
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1 text-yellow-400 text-xs">
|
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
||||||
<FaStar className="w-3 h-3 fill-current" />
|
{movie.title}
|
||||||
<span>{movie.vote_average.toFixed(1)}</span>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Link>
|
||||||
{movie.favorite && (
|
</motion.div>
|
||||||
<div className="w-2 h-2 bg-red-500 rounded-full" title="Ulubione" />
|
</div>
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue