Birthday
Birthdays are things we can't live without. Display all birthdays of friends
Birthday
Upcoming
Installation
Run the following command
It will create a new file birthday.tsx
inside the components/cards/birthday.tsx
directory.
mkdir -p components/cards && touch components/cards/birthday.tsx
Add this snippet to your globals.css
file
.bar {
overflow-x: auto; /* Allows horizontal scrolling */
scrollbar-width: thin; /* For Firefox, makes the scrollbar thinner */
scrollbar-color: transparent transparent; /* For Firefox, custom scrollbar colors */
}
/* For WebKit browsers (Chrome, Safari, Edge) */
.bar::-webkit-scrollbar {
height: 8px; /* Adjust scrollbar height for horizontal scrolling */
}
.bar::-webkit-scrollbar-track {
background: transparent; /* The track is transparent */
}
.bar::-webkit-scrollbar-thumb {
@apply bg-primary; /* The scrollbar thumb color (more visible) */
border-radius: 10px; /* Rounds the edges of the scrollbar */
border: 2px solid transparent;
}
Paste the code
Open the newly created file and paste the following code:
"use client";
import React, { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Grip, Plus, X } from "lucide-react";
interface Birthday {
name: string;
image: string;
dateOfBirth: string;
}
const months = [
{
id: 1,
name: "Jan",
items: [
{
name: "Alice",
image: "https://avatar.iran.liara.run/public/82",
dateOfBirth: "1990-01-15",
},
{
name: "Bob",
image: "https://avatar.iran.liara.run/public/30",
dateOfBirth: "1988-01-24",
},
{
name: "Charlie",
image: "https://avatar.iran.liara.run/public/31",
dateOfBirth: "1992-01-10",
},
],
},
{
id: 2,
name: "Feb",
items: [
{
name: "David",
image: "https://avatar.iran.liara.run/public/3",
dateOfBirth: "1991-02-05",
},
],
},
{
id: 3,
name: "Mar",
items: [
{
name: "Eva",
image: "https://avatar.iran.liara.run/public/60",
dateOfBirth: "1993-03-20",
},
{
name: "Frank",
image: "https://avatar.iran.liara.run/public/39",
dateOfBirth: "1989-03-15",
},
{
name: "Grace",
image: "https://avatar.iran.liara.run/public/51",
dateOfBirth: "1995-03-30",
},
{
name: "Hannah",
image: "https://avatar.iran.liara.run/public/94",
dateOfBirth: "1994-03-22",
},
],
},
{
id: 4,
name: "Apr",
items: [
{
name: "Ivy",
image: "https://avatar.iran.liara.run/public/92",
dateOfBirth: "1992-04-11",
},
{
name: "Jack",
image: "https://avatar.iran.liara.run/public/36",
dateOfBirth: "1987-04-05",
},
],
},
{
id: 5,
name: "May",
items: [
{
name: "Liam",
image: "https://avatar.iran.liara.run/public/75",
dateOfBirth: "1990-05-03",
},
{
name: "Mia",
image: "https://avatar.iran.liara.run/public/96",
dateOfBirth: "1992-05-15",
},
{
name: "Noah",
image: "https://avatar.iran.liara.run/public/47",
dateOfBirth: "1985-05-25",
},
],
},
{
id: 6,
name: "Jun",
items: [
{
name: "Olivia",
image: "https://avatar.iran.liara.run/public/58",
dateOfBirth: "1991-06-09",
},
],
},
{
id: 7,
name: "Jul",
items: [
{
name: "Penny",
image: "https://avatar.iran.liara.run/public/13",
dateOfBirth: "1989-07-20",
},
{
name: "Quinn",
image: "https://avatar.iran.liara.run/public/93",
dateOfBirth: "1993-07-04",
},
{
name: "Riley",
image: "https://avatar.iran.liara.run/public/67",
dateOfBirth: "1988-07-11",
},
{
name: "Sophia",
image: "https://avatar.iran.liara.run/public/89",
dateOfBirth: "1992-07-15",
},
{
name: "Toby",
image: "https://avatar.iran.liara.run/public/18",
dateOfBirth: "1994-07-29",
},
],
},
{
id: 8,
name: "Aug",
items: [
{
name: "Uma",
image: "https://avatar.iran.liara.run/public/15",
dateOfBirth: "1995-08-02",
},
{
name: "Violet",
image: "https://avatar.iran.liara.run/public/79",
dateOfBirth: "1993-08-21",
},
],
},
{
id: 9,
name: "Sep",
items: [
{
name: "Will",
image: "https://avatar.iran.liara.run/public/4",
dateOfBirth: "1987-09-19",
},
{
name: "Xander",
image: "https://avatar.iran.liara.run/public/7",
dateOfBirth: "1991-09-14",
},
],
},
{
id: 10,
name: "Oct",
items: [
{
name: "Zoe",
image: "https://avatar.iran.liara.run/public/57",
dateOfBirth: "1985-10-03",
},
{
name: "Aaron",
image: "https://avatar.iran.liara.run/public/21",
dateOfBirth: "1990-10-16",
},
{
name: "Bella",
image: "https://avatar.iran.liara.run/public/91",
dateOfBirth: "1992-10-22",
},
{
name: "Cody",
image: "https://avatar.iran.liara.run/public/26",
dateOfBirth: "1994-10-30",
},
],
},
{
id: 11,
name: "Nov",
items: [
{
name: "Yara",
image: "https://avatar.iran.liara.run/public/20",
dateOfBirth: "1992-11-12",
},
],
},
{
id: 12,
name: "Dec",
items: [
{
name: "Diana",
image: "https://avatar.iran.liara.run/public/95",
dateOfBirth: "1991-12-05",
},
{
name: "Ethan",
image: "https://avatar.iran.liara.run/public/41",
dateOfBirth: "1989-12-15",
},
{
name: "Fiona",
image: "https://avatar.iran.liara.run/public/88",
dateOfBirth: "1992-12-25",
},
],
},
];
const getCurrentMonth = () => {
const date = new Date();
return date.getMonth(); // 0 = January, 1 = February, ..., 11 = December
};
const transition = { type: "spring", bounce: 0, duration: 0.4 };
const Month = ({
month,
index,
}: {
month: (typeof months)[number];
index: number;
}) => {
const firstThree = month.items.slice(0, 3);
const remainingBirthdays = month.items.length - firstThree.length;
const currentMonthIndex = getCurrentMonth() === index;
return (
<motion.div
layoutId={`month-${month.name}`}
key={month.id}
className="flex flex-col gap-2"
>
<p className="flex items-center gap-2">
{currentMonthIndex && (
<span className="size-2 rounded-full bg-red-500" />
)}
<span className="uppercase text-muted-foreground text-sm font-medium">
{month.name}
</span>
</p>
<div className="flex">
{firstThree.map((item, index) => (
<div
key={index}
style={{
width: 40,
height: 40,
}}
className="size-10 center rounded-full text-muted-foreground -ml-2 font-bold text-sm"
>
<img
src={item.image}
alt={item.name}
className="rounded-full size-10"
/>
</div>
))}
{remainingBirthdays > 0 && (
<div className="size-10 center rounded-full text-muted-foreground -ml-2 dark:bg-neutral-300 bg-neutral-800 font-bold text-sm">
<span>
<Plus className="size-4" />
</span>{" "}
{remainingBirthdays}
</div>
)}
</div>
</motion.div>
);
};
interface HeaderProps {
isOpen: boolean;
onOpen: () => void;
}
const Header = ({ isOpen, onOpen }: HeaderProps) => {
const toggleMenu = () => {
onOpen();
};
const variants = {
initial: {
opacity: 0,
y: -10,
},
enter: {
opacity: 1,
y: 0,
},
exit: {
opacity: 0,
y: -10,
},
};
return (
<div className="flex items-center justify-between p-4">
<motion.p className="text-3xl font-semibold">
<AnimatePresence mode="wait">
{isOpen ? (
<motion.span
key="2025" // Unique key for "2025"
variants={variants}
initial="initial"
animate="enter"
exit="exit"
transition={{
duration: 0.3, // You can adjust the timing
}}
>
2025
</motion.span>
) : (
<motion.span
key="Birthday" // Unique key for "Birthday"
variants={variants}
initial="initial"
animate="enter"
exit="exit"
transition={{
duration: 0.3,
}}
>
Birthday
</motion.span>
)}
</AnimatePresence>
</motion.p>
<button
className="size-12 p-2 center gap-2 cursor-pointer bg-primary text-primary-foreground rounded-full flex-col"
onClick={toggleMenu}
>
{Array.from({ length: 2 }).map((_, index) => {
const rotateAngle = index % 2 === 0 ? 45 : -45;
const changeY = index % 2 === 0 ? 5.5 : -5.5;
return (
<motion.span
key={index}
animate={{
rotate: isOpen ? rotateAngle : 0,
y: isOpen ? changeY : 0,
}}
className="w-8 !h-[3px] bg-primary-foreground"
/>
);
})}
</button>
</div>
);
};
const Birthday = () => {
const [status, setStatus] = useState<string>("idle");
const isOpen = status === "open";
const [selected, setSelected] = useState<null | Birthday>(null);
const [upcomingBirthdays, setUpcomingBirthdays] = useState<Birthday[]>([]);
useEffect(() => {
const monthIndex = getCurrentMonth(); // Get current month index
setUpcomingBirthdays(months[monthIndex].items); // Set items for current month
}, []);
const calculateAge = (dateOfBirth: string): number => {
if (!dateOfBirth) return 0; // Handle empty dateOfBirth
const birthDate = new Date(dateOfBirth);
if (isNaN(birthDate.getTime())) return 0; // Check if the date is valid
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDifference = today.getMonth() - birthDate.getMonth();
// Adjust age if the birthday hasn't occurred yet this year
if (
monthDifference < 0 ||
(monthDifference === 0 && today.getDate() < birthDate.getDate())
) {
age--;
}
return age;
};
const getRemainingDays = (dateOfBirth: string): number => {
if (!dateOfBirth) return -1; // Handle empty dateOfBirth
const today = new Date();
const birthDateThisYear = new Date(
today.getFullYear(),
new Date(dateOfBirth).getMonth(),
new Date(dateOfBirth).getDate()
);
// If the birthday has already occurred this year, calculate for next year
if (birthDateThisYear < today) {
birthDateThisYear.setFullYear(today.getFullYear() + 1);
}
// Calculate the difference in time
const differenceInTime = birthDateThisYear.getTime() - today.getTime();
if (differenceInTime < 0) return -1; // Handle any negative difference
// Convert time difference to days
return Math.ceil(differenceInTime / (1000 * 3600 * 24));
};
const formatDate = (dateOfBirth: string): string => {
const date = new Date(dateOfBirth);
const options: Intl.DateTimeFormatOptions = {
month: "long", // 'long' gives full month name
day: "numeric", // numeric gives the day without leading zeros
};
return date.toLocaleDateString("en-US", options);
};
return (
<div className="h-full w-full center bg-primary">
<AnimatePresence>
{isOpen ? (
<motion.div
className="p-4 bg-primary text-primary-foreground"
layoutId="wrapper"
style={{ borderRadius: 22, height: 430, width: 500 }}
>
<Header isOpen={isOpen} onOpen={() => setStatus("idle")} />
<motion.div
layoutId="birthdays-container"
className="grid grid-cols-3 gap-4 overflow-x-clip bar px-4"
>
{months.map((month, index) => (
<Month key={month.id} month={month} index={index} />
))}
</motion.div>
</motion.div>
) : (
<motion.div
layoutId="wrapper"
className="p-4 bg-primary text-primary-foreground flex flex-col gap-5"
style={{ borderRadius: 22, width: 500, height: 510 }}
>
<Header isOpen={isOpen} onOpen={() => setStatus("open")} />
<div className="relative">
<motion.div
layoutId="birthdays-container"
className="flex overflow-x-scroll bar gap-4 px-4"
>
{months.map((month, index) => (
<Month key={month.id} month={month} index={index} />
))}
</motion.div>
<div className="h-full w-10 bg-gradient-to-r from-primary to-transparent absolute top-0 left-0" />
<div className="h-full w-10 bg-gradient-to-l from-primary to-transparent absolute top-0 right-0" />
</div>
<div className="flex-1 flex flex-col gap-5 relative">
<p className="uppercase text-xs">Upcoming</p>
<div className="flex flex-col gap-2">
<AnimatePresence>
{upcomingBirthdays.map((birthday, index) => (
<motion.div
className="hover:bg-neutral-800/80 dark:bg-neutral-200 bg-neutral-800 p-2 rounded-xl flex gap-2 cursor-pointer hover:dark:bg-neutral-300/80 text-primary-foreground"
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ delay: (3 - index) * 0.06 }} // for it to have the reversed staggering
layoutId={`birthday-${birthday.name}`}
onClick={() => setSelected(birthday)}
>
<motion.div
layoutId={`profile-${birthday.name}`}
className="size-10 rounded-full min-w-10"
>
<img
src={birthday.image}
alt={birthday.name}
className="rounded-full w-10 h-10"
/>
</motion.div>
<motion.div className="flex justify-between items-center flex-1">
<p className="flex flex-col">
<motion.span
layoutId={`profile-name-${birthday.name}`}
>
{birthday.name}
</motion.span>
<motion.span
layoutId={`profile-dob-${birthday.name}`}
className="text-xs"
>
{formatDate(birthday.dateOfBirth)}
</motion.span>
</p>
<motion.p layoutId={`profile-age-${birthday.name}`}>
{calculateAge(birthday.dateOfBirth)}y
</motion.p>
</motion.div>
</motion.div>
))}
</AnimatePresence>
</div>
<AnimatePresence>
{selected !== null && (
<motion.div
layoutId={`birthday-${selected.name}`}
className="absolute -left-4 h-[calc(100%_+16px)] w-[calc(100%_+32px)] rounded-[22px] p-4 center flex-col gap-5 text-primary-foreground dark:bg-neutral-200 bg-neutral-800"
>
<motion.button
layout
onClick={() => setSelected(null)}
initial={{ opacity: 0, x: -20, y: 10 }}
animate={{ opacity: 1, x: 0, y: 0 }}
exit={{ opacity: 0, x: -20, y: 10 }}
transition={{ ...transition, delay: 0.15 }}
whileTap={{
scale: 0.9,
transition: { ...transition, duration: 0.2 },
}}
className="size-8 absolute top-5 right-5"
>
<X className="size-6 text-tight text-primary-foreground" />
</motion.button>
<div className="gap-2 center flex-col">
<motion.div
layoutId={`profile-${selected.name}`}
className="size-20 rounded-full min-w-10"
>
<img
src={selected.image}
alt={selected.name}
className="rounded-full size-full"
/>
</motion.div>
<motion.div
layoutId={`profile-info-${selected.name}`}
className="flex flex-col gap-2 flex-1"
>
<p className="flex flex-col text-center">
<motion.span
layoutId={`profile-name-${selected.name}`}
className="font-semibold text-2xl"
>
{selected.name}
</motion.span>
<motion.span
layoutId={`profile-dob-${selected.name}`}
className="text-base"
>
{formatDate(selected.dateOfBirth)} .{" "}
<motion.span className="text-xs">
{getRemainingDays(selected.dateOfBirth)} days
</motion.span>
</motion.span>
</p>
<motion.p
layoutId={`profile-age-${selected.name}`}
className="text-2xl font-semibold"
>
{calculateAge(selected.dateOfBirth)}years old
</motion.p>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default Birthday;
Credits
Built by Bossadi Zenith