Booking
Booking morph animated cards
Installation
Run the following command
It will create a new file booking.tsx
inside the components/cards/booking.tsx
directory.
mkdir -p components/cards && touch components/cards/booking.tsx
Paste the code
Open the newly created file and paste the following code:
"use client";
import { cn } from "@/lib/utils";
import zenith from "@/public/zenith.jpeg";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle2, Loader } from "lucide-react";
import Image from "next/image";
import { ChangeEvent, FormEvent, useState } from "react";
type User = {
username: string;
email: string;
};
const Booking = () => {
const [isOpen, setIsOpen] = useState(false);
const [isSelected, setIsSelected] = useState("");
const [user, setUser] = useState<User>({
username: "",
email: "",
});
const [loading, setLoading] = useState(false);
const [isConfirmed, setIsConfirmed] = useState(false);
const [isActive, setIsActive] = useState(0);
const bookingVariant = {
initial: {
opacity: 0,
y: -200,
},
animate: {
opacity: 1,
y: -320,
transition: {
type: "spring",
stiffness: 260,
damping: 20,
delay: 0.2,
duration: 5,
ease: [0.9, 0.1, 0.25, 1],
},
},
exit: {
opacity: 0,
y: -180,
transition: {
duration: 0.2,
},
},
};
const times = ["1:00pm", "2:00pm", "3:00pm", "4:00pm"];
const days = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setUser((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
try {
setLoading(true);
// validate form
if (!username || !email) {
throw new Error("Please fill out all fields");
}
// make some fake async call with promise for 5
await new Promise((resolve) => setTimeout(resolve, 3000));
setIsConfirmed(true);
setTimeout(() => {
setIsOpen(false);
}, 2000);
} catch (error: any) {
} finally {
setLoading(false);
}
};
const { username, email } = user;
return (
<div className="h-full flex items-end justify-center w-full">
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
className="absolute"
>
<defs>
<filter id="goo">
<feGaussianBlur
in="SourceGraphic"
stdDeviation="10"
result="blur"
/>
<feColorMatrix
in="blur"
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
result="goo"
/>
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
<div
style={{
filter: "url(#goo)",
}}
>
<AnimatePresence>
{isOpen && (
<motion.div
className="h-[300px] bg-primary text-primary-foreground absolute rounded-3xl overflow-hidden -z-10 w-[500px] p-5 mx-auto "
variants={bookingVariant}
initial="initial"
animate="animate"
exit="exit"
>
<AnimatePresence>
{isSelected.length === 0 && (
<motion.div
exit={{
opacity: 0,
y: -100,
transition: {
duration: 0.2,
},
}}
className="w-full h-full gap-10 flex"
>
<div className="flex-1">
<div className="w-full flex items-center justify-between text-primary-foreground/50">
<p>
Wed <span>21</span>
</p>
</div>
<div className="grid grid-cols-5 gap-3 text-primary-foreground/80">
{days.map((day) => {
// some code here
return (
<motion.button
key={day}
className="py-1.5 rounded-xl flex-1"
>
{day}
</motion.button>
);
})}
</div>
<div className="grid grid-cols-5 gap-3">
{Array.from({ length: 20 }).map((_, index) => {
//some code here
return (
<div
key={index}
className={cn(
"h-10 w-10 text-primary-foreground center rounded-xl cursor-pointer hover:bg-white/20 transition-all duration-150",
isActive === index &&
"bg-white/50 hover:bg-background"
)}
onClick={() => {
setIsActive(index);
}}
>
{index + 1}
</div>
);
})}
</div>
</div>
<div className="flex-[0.6] bg-primary flex flex-col gap-10">
<div className="w-full flex items-center justify-between text-primary-foreground/50">
<p>
Wed <span>21</span>
</p>
</div>
<div className="flex flex-col gap-4 ">
{times.map((time) => {
// some code here
return (
<motion.button
whileTap={{
scale: 0.9,
}}
onClick={() => setIsSelected(time)}
key={time}
className="bg-secondary backdrop-blur text-black dark:text-white px-4 py-1.5 rounded-xl"
>
{time}
</motion.button>
);
})}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{isSelected && (
<motion.div
className="text-primary-foreground h-full w-full flex gap-2"
initial={{
y: -0,
opacity: 0,
}}
animate={{
y: isConfirmed ? -400 : 0,
opacity: 1,
transition: {
duration: 0.5,
},
}}
exit={{
opacity: 0,
y: -100,
transition: {
duration: 0.2,
},
}}
>
<div className="flex flex-[.65] flex-col space-y-4 text-primary-foreground/90">
<h1 className="textw">Confirm your booking</h1>
<div className="space-y-2">
<div className="flex gap-2 items-center">
<div className="h-10 w-10 rounded-full relative overflow-hidden">
<Image src={zenith} alt="Bossadi zenith" fill />
</div>
<span className="font-semibold">Bossadi Zenith</span>
</div>
<h2 className="flex flex-col gap-2">
<span> Monday, August {isActive + 1} 2024</span>{" "}
<span className="text-lg text-muted-foreground">
{isSelected}
</span>
</h2>
</div>
</div>
<div className="flex flex-1">
<form
onSubmit={handleSubmit}
className="flex flex-col gap-5 w-full"
>
<div className="flex flex-col gap-2">
<label
htmlFor="name"
className="font-medium text-primary-foreground/70"
>
Your name
</label>
<input
type="text"
id="name"
name="username"
value={username}
onChange={onChange}
required
disabled={loading}
className="h-14 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed bg-transparent border-2 px-5 text-primary-foreground/70 outline-none border-background"
placeholder="Your name"
/>
</div>
<div className="flex flex-col gap-2 w-full">
<label
htmlFor="name"
className="font-medium text-primary-foreground/70"
>
Your email
</label>
<input
type="email"
id="name"
name="email"
value={email}
onChange={onChange}
required
placeholder="Your email"
disabled={loading}
className="h-14 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed bg-transparent border-2 px-5 text-primary-foreground/70 outline-none border-background"
/>
</div>
<motion.button
type="submit"
disabled={loading}
className="bg-background dark:text-white text-black flex items-center gap-2 justify-center px-4 h-14 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading && (
<Loader className="animate-spin h-4 w-4" />
)}{" "}
Book Now
</motion.button>
</form>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{isConfirmed && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
className="h-full w-full black absolute top-0 left-0 flex flex-col gap-4 items-center justify-center"
>
<CheckCircle2
className="text-primary-foreground"
fill="white"
stroke="black"
/>
<h1 className="text-primary-foreground font-bold flex flex-col text-center text-xl">
<span>Booking confirmed!</span>
<span>Looking forward to chatting!</span>
</h1>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
<div className="h-20 flex items-center justify-center">
<div className="flex items-center justify-between bg-primary rounded-2xl mx-auto z-10 p-1 w-[500px] px-2.5">
<motion.div
animate={{
height: 50,
}}
className="bg-primary bg-black rounded-lg max-w-[50px] min-w-[50px] flex items-center justify-center"
>
<div className="h-4 rounded w-4 bg-white dark:bg-black rotate-45" />
</motion.div>
<motion.button
className="bg-secondary text-black dark:text-white px-4 py-1.5 rounded-xl"
onClick={() => setIsOpen((prev) => !prev)}
>
Book a call
</motion.button>
</div>
</div>
</div>
</div>
);
};
export default Booking;
Credits
Built by Bossadi Zenith