Last active
August 23, 2024 14:22
-
-
Save alishahlakhani/74d61a918714a4047de0ff3cb555abf5 to your computer and use it in GitHub Desktop.
Create a Tarot card deck selection page using Framer Motion and Nextjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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