Framer ground
Birthdays are things we can't live without. Display all birthdays of friends
Birthday
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Upcoming
It will create a new file birthday.tsx inside the components/cards/birthday.tsx directory.
birthday.tsx
components/cards/birthday.tsx
mkdir -p components/cards && touch components/cards/birthday.tsx
globals.css
.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; }
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;
Built by Bossadi Zenith