Skip to content

Instantly share code, notes, and snippets.

@alishahlakhani
Last active August 23, 2024 14:22
Show Gist options
  • Save alishahlakhani/74d61a918714a4047de0ff3cb555abf5 to your computer and use it in GitHub Desktop.
Save alishahlakhani/74d61a918714a4047de0ff3cb555abf5 to your computer and use it in GitHub Desktop.
Create a Tarot card deck selection page using Framer Motion and Nextjs
"use client";
import React, { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import Image from "next/image";
import clsx from "clsx";
type Props = {
onPick?: (card: string | null) => void;
onSelect?: (card: string) => void;
};
const ShuffledDeck = [
{
title: "The Fool",
image:
"https://images.unsplash.com/photo-1720725727156-f68df0468436?q=80&w=2535&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Magician",
image:
"https://images.unsplash.com/photo-1722196174475-6834f9157d51?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The High Priestess",
image:
"https://images.unsplash.com/photo-1722156772564-8f921329cc12?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Empress",
image:
"https://images.unsplash.com/photo-1646579352833-cb46fb461f64?q=80&w=2672&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Emperor",
image:
"https://images.unsplash.com/photo-1701316613369-6ea5c0d96b98?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3DD",
},
{
title: "The Hierophant",
image:
"https://images.unsplash.com/photo-1722104784480-52b6fc3e3a34?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Lovers",
image:
"https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Chariot",
image:
"https://plus.unsplash.com/premium_photo-1719017469871-a1bd6615dabe?q=80&w=2525&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "Strength",
image:
"https://plus.unsplash.com/premium_photo-1719017469915-b7501a0d6147?q=80&w=2525&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Hermit",
image:
"https://plus.unsplash.com/premium_photo-1719017472059-8d1d0ab3cba5?q=80&w=2525&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "Wheel of Fortune",
image:
"https://images.unsplash.com/photo-1612323272007-3e7c28f6eb05?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "Justice",
image:
"https://images.unsplash.com/photo-1479330173277-6c90ae1bb2c0?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Hanged Man",
image:
"https://images.unsplash.com/photo-1493690314206-255f1df89427?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "Death",
image:
"https://images.unsplash.com/photo-1721817269931-eafc6308899e?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDI1fHFQWXNEenZKT1ljfHxlbnwwfHx8fHw%3D",
},
{
title: "Temperance",
image:
"https://images.unsplash.com/photo-1721566364814-7345a56e7ab0?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Devil",
image:
"https://images.unsplash.com/photo-1716043657397-92666764b512?q=80&w=2614&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Tower",
image:
"https://images.unsplash.com/photo-1534312527009-56c7016453e6?q=80&w=2454&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Star",
image:
"https://images.unsplash.com/flagged/photo-1567400358593-9e6382752ea2?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Moon",
image:
"https://images.unsplash.com/photo-1563089145-599997674d42?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The Sun",
image:
"https://images.unsplash.com/photo-1516464278939-6c47180c46eb?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "Judgment",
image:
"https://images.unsplash.com/photo-1511447333015-45b65e60f6d5?q=80&w=2510&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
{
title: "The World",
image:
"https://images.unsplash.com/photo-1546146477-15a587cd3fcb?q=80&w=2160&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
},
];
export default function CardsDeckViewer(props: Props) {
const { onPick, onSelect } = props;
const [picked, setPicked] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const numberOfDivs = ShuffledDeck.length; // Change this value to adjust the number of divs
const radius = 120; // Define the radius of the semi-circle
const firstCardAngle = 90;
// How much open the fan is.
const openAngle = 180;
function getRandomArbitrary(min: number, max: number) {
return Math.random() * (max - min) + min;
}
function handleRandomPick() {
const randomCardIndex = Math.round(
getRandomArbitrary(0, ShuffledDeck.length)
);
const pickedCard = ShuffledDeck[randomCardIndex].title;
setPicked(pickedCard);
}
return (
<div className="relative h-full w-full bg-[#181B1D] px-6 py-8 rounded-2xl">
<div className="mt-[20rem] h-full w-full flex items-end justify-center ">
<AnimatePresence>
{ShuffledDeck.map((card, index) => {
const fixedAngle =
(openAngle / (numberOfDivs - 1)) * index - firstCardAngle;
const fixedtranslateX =
-Math.sin(fixedAngle * (Math.PI / 180)) * radius;
const fixedtranslateY =
-Math.cos(fixedAngle * (Math.PI / 180)) * radius;
const fixedrotate = -(
(openAngle / (numberOfDivs - 1)) * index -
firstCardAngle
);
const variants = {
selected: {
translateX: 0,
translateY: -100,
scale: 2.5,
zIndex: 10,
transition: { duration: 0.5 },
},
unselected: {
translateX: fixedtranslateX,
translateY: fixedtranslateY,
rotate: fixedrotate,
opacity: 0,
transition: { duration: 0.5 },
},
initial: {
translateX: fixedtranslateX,
translateY: fixedtranslateY,
rotate: fixedrotate,
transition: { duration: 0.2 },
},
};
function handleCardSelect() {
if (card.title === picked) {
setPicked(null);
setSelected(null);
onPick && onPick(null);
} else if (picked === null) {
setPicked(card.title);
onPick && onPick(card.title);
} else {
setPicked(null);
setSelected(null);
}
}
function handleAnimationEnd(
defination: "selected" | "preSelected" | "initial"
) {
switch (defination) {
case "selected":
if (selected && onSelect)
setTimeout(() => {
onSelect(selected);
}, 1000);
return;
default:
return;
}
}
return (
<motion.div
key={card.title}
data-test={card.title}
animate={
picked === card.title
? "selected"
: picked !== null
? "unselected"
: "initial"
}
variants={variants}
onClick={handleCardSelect}
className={clsx(
"absolute bg-black/30 cursor-pointer rounded-md overflow-hidden h-[6.6rem] w-[4.6rem]",
{
"opacity-100": picked === card.title,
grayscale: picked !== null && picked !== card.title,
}
)}
>
<div
className="cursor-pointer w-full h-full relative"
style={{
perspective: "1000px",
}}
>
<motion.div
exit={{ opacity: 0 }}
id="card"
key={`card-${card.title}`}
className="relative w-full h-full"
variants={{
selected: {
transition: { duration: 0.4 },
},
initial: {
rotateY: 0,
transition: { duration: 0.4 },
},
}}
animate={picked === card.title ? "selected" : "initial"}
style={{
transformStyle: "preserve-3d",
}}
>
<motion.div
exit={{ opacity: 0 }}
id="front"
key={`front-${card.title}`}
className={clsx("absolute w-full h-full border-2", {
"border-white": selected === card.title,
})}
variants={{
selected: {
transition: { duration: 0.4 },
scaleX: 1,
opacity: 1,
},
initial: {
scaleX: -1,
opacity: 0,
transition: { duration: 0.4 },
},
}}
animate={
card.title === picked && picked === selected
? "selected"
: "initial"
}
style={{
transformStyle: "preserve-3d",
backfaceVisibility: "hidden",
}}
>
<Image
data-card={card.title}
style={{}}
fill
src={card.image}
alt={card.title}
/>
</motion.div>
<motion.div
id="back"
exit={{ opacity: 0 }}
key={`back-${card.title}`}
className={clsx(
"absolute w-full h-full border-2 border-black/20 hover:border-white",
{
"border-white": picked === card.title,
}
)}
variants={{
selected: {
transition: { duration: 0.4 },
scaleX: -1,
opacity: 0,
},
preSelected: {
transition: { duration: 0.4 },
opacity: 1,
},
initial: {
transition: { duration: 0.4 },
opacity: 1,
},
}}
animate={
card.title === picked && picked === selected
? "selected"
: "initial"
}
onAnimationComplete={handleAnimationEnd}
style={{
backfaceVisibility: "hidden",
transformStyle: "preserve-3d",
}}
>
<Image
data-card={card.title}
fill
src={
"https://images.unsplash.com/photo-1578073273382-f847b29d2192?q=80&w=2568&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
}
alt={card.title}
/>
</motion.div>
</motion.div>
</div>
</motion.div>
);
})}
</AnimatePresence>
<div className="flex gap-2">
<button
className="relative top-6 p-2 rounded-sm bg-white hover:bg-white/80 text-black"
onClick={handleRandomPick}
>
{picked ? "Pick another" : "Choose for me"}
</button>
{picked && (
<button
className="relative top-6 p-2 rounded-sm bg-fuchsia-800 hover:bg-fuchsia-800/80 text-fuchsia-300"
onClick={(_e) => {
if (selected == picked) {
setSelected(null);
} else {
setSelected(picked);
}
}}
>
Reveal Card
</button>
)}
</div>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment