Files
no-whatever/src/components/SwipeableCard.tsx
T

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>
);
}