119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useRef } from "react";
|
|
import {
|
|
motion,
|
|
useMotionValue,
|
|
useTransform,
|
|
animate,
|
|
PanInfo,
|
|
MotionValue,
|
|
} from "framer-motion";
|
|
import RestaurantCard from "./RestaurantCard";
|
|
import { Restaurant, SwipeDirection } from "@/types";
|
|
|
|
const SWIPE_THRESHOLD = 120;
|
|
const getExitX = () =>
|
|
typeof window !== "undefined" ? window.innerWidth * 1.5 : 600;
|
|
const ROTATION_RANGE = 18;
|
|
|
|
interface SwipeableCardProps {
|
|
restaurant: Restaurant;
|
|
isTop: boolean;
|
|
onSwipe: (direction: SwipeDirection) => void;
|
|
registerSwipe?: (fn: (direction: SwipeDirection) => void) => void;
|
|
likeCount: number;
|
|
}
|
|
|
|
function SwipeOverlay({ x }: { x: MotionValue<number> }) {
|
|
const likeOpacity = useTransform(x, [0, SWIPE_THRESHOLD], [0, 1]);
|
|
const nopeOpacity = useTransform(x, [-SWIPE_THRESHOLD, 0], [1, 0]);
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
className="pointer-events-none absolute inset-0 z-10 flex items-start justify-start rounded-2xl border-4 border-emerald-400 p-6"
|
|
style={{ opacity: likeOpacity }}
|
|
>
|
|
<span className="rounded-lg border-3 border-emerald-400 px-3 py-1 text-2xl font-extrabold tracking-wide text-emerald-400">
|
|
LIKE
|
|
</span>
|
|
</motion.div>
|
|
<motion.div
|
|
className="pointer-events-none absolute inset-0 z-10 flex items-start justify-end rounded-2xl border-4 border-rose-400 p-6"
|
|
style={{ opacity: nopeOpacity }}
|
|
>
|
|
<span className="rounded-lg border-3 border-rose-400 px-3 py-1 text-2xl font-extrabold tracking-wide text-rose-400">
|
|
NOPE
|
|
</span>
|
|
</motion.div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function SwipeableCard({
|
|
restaurant,
|
|
isTop,
|
|
onSwipe,
|
|
registerSwipe,
|
|
likeCount,
|
|
}: SwipeableCardProps) {
|
|
const x = useMotionValue(0);
|
|
const rotate = useTransform(x, [-300, 300], [-ROTATION_RANGE, ROTATION_RANGE]);
|
|
const opacity = useTransform(x, [-300, -100, 0, 100, 300], [0.5, 1, 1, 1, 0.5]);
|
|
|
|
const isSwiping = useRef(false);
|
|
|
|
const flyOut = (direction: SwipeDirection) => {
|
|
if (isSwiping.current) return;
|
|
isSwiping.current = true;
|
|
const exit = getExitX();
|
|
const exitX = direction === "right" ? exit : -exit;
|
|
animate(x, exitX, {
|
|
type: "spring",
|
|
stiffness: 600,
|
|
damping: 40,
|
|
onComplete: () => onSwipe(direction),
|
|
});
|
|
};
|
|
|
|
if (registerSwipe) {
|
|
registerSwipe(flyOut);
|
|
}
|
|
|
|
const handleDragEnd = (_: unknown, info: PanInfo) => {
|
|
const offsetX = info.offset.x;
|
|
if (offsetX > SWIPE_THRESHOLD) {
|
|
flyOut("right");
|
|
} else if (offsetX < -SWIPE_THRESHOLD) {
|
|
flyOut("left");
|
|
} else {
|
|
animate(x, 0, { type: "spring", stiffness: 500, damping: 30 });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
className="absolute inset-0"
|
|
style={{
|
|
x,
|
|
rotate,
|
|
opacity,
|
|
zIndex: isTop ? 10 : 0,
|
|
cursor: isTop ? "grab" : "default",
|
|
}}
|
|
drag={isTop ? "x" : false}
|
|
dragConstraints={{ left: 0, right: 0 }}
|
|
dragElastic={0.9}
|
|
onDragEnd={handleDragEnd}
|
|
whileDrag={{ cursor: "grabbing" }}
|
|
initial={isTop ? { scale: 1 } : { scale: 0.95, y: 16 }}
|
|
animate={isTop ? { scale: 1, y: 0 } : { scale: 0.95, y: 16 }}
|
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
>
|
|
<SwipeOverlay x={x} />
|
|
<RestaurantCard restaurant={restaurant} likeCount={likeCount} />
|
|
</motion.div>
|
|
);
|
|
}
|