Building a Birthday Wish App with Next.js

Day 3: Birthday Wish— 30 Days of 30 Projects Challenge

Asharib Ali
10 min readSep 4, 2024

Hey everyone, I hope you’re all doing well and enjoying your coding journey, Am I right? By the way, today marks the 3rd day of the 30-day of 30-projects challenge. Our 3rd project will be creating a mini Next.js application — a birthday wish. Please make sure to clap and comment on this blog post. It motivates me to create more amazing content like this, Let’s get started straight away.

Overview of the Mini Next.js Application

Our Birthday Wish application allows users to:

  • Celebrate the birthday
  • Display confetti on the screen
  • Light the candles & Pop the balloons

Tech-Stack Used:

  • Next.js: A React framework for building full-stack web applications.
  • React: A JavaScript library for building user interfaces.
  • Tailwind CSS: A utility-first CSS framework for styling.
  • Shadcn UI: Beautifully designed tailwindcss components that you can copy and paste into your application.
  • Vercel: For deploying the Nextjs web application.

Initialize a Nextjs project

Start by following this guide to set up the new project.

  • Go to the “components” folder.
  • Create a new file named “birthday-wish.tsx”.
  • This file will manage the entire functionality of the project.

We will go through the code step-by-step to make it easy to understand.

Component Breakdown

Import Statements

'use client'

import React, { useState, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
import { motion, AnimatePresence } from 'framer-motion'
import dynamic from 'next/dynamic'
import { FaBirthdayCake, FaGift } from 'react-icons/fa'
import { GiBalloons } from 'react-icons/gi'
  • “use client”: It enables client-side rendering for this component.
  • useState, useEffect: React hooks for managing state and browser effect.
  • Button, Card: Custom UI components from Shadcn UI.
  • Framer Motion: Animate and Presence from Framer package.
  • FaBirthdayCake, FaGift, GiBallons: Birthday Icons from the React Icons library.

Define Types

type ConfettiProps = {
width: number
height: number
}
  • The ConfettiProps type is defined to specify the properties that the Confetti component will accept.
  • It includes two properties, width and height, both of which are required and must be numbers.

Dynamic Import & Variables

const DynamicConfetti = dynamic(() => import('react-confetti'), { ssr: false })

const candleColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
const balloonColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
const confettiColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE']
  • The DynamicConfetti component is dynamically imported from the 'react-confetti' library, and ssr: false ensures it is not rendered on the server but only on the client-side.
  • Three arrays, candleColors, balloonColors, and confettiColors, are defined to hold specific color values in hexadecimal format for candles, balloons, and confetti, respectively.
  • These arrays are used to consistently apply the same color scheme across different elements in the application.

State variables


const [candlesLit, setCandlesLit] = useState<number>(0)
const [balloonsPoppedCount, setBalloonsPoppedCount] = useState<number>(0)
const [showConfetti, setShowConfetti] = useState<boolean>(false)
const [windowSize, setWindowSize] = useState<ConfettiProps>({ width: 0, height: 0 })
const [celebrating, setCelebrating] = useState<boolean>(false)
  • candlesLit tracks the number of candles that have been lit, starting at 0.
  • balloonsPoppedCount tracks the number of balloons that have been popped, also starting at 0.
  • showConfetti controls whether confetti is displayed or not, initially set to false.
  • windowSize stores the current width and height of the window, which is important for displaying confetti correctly.
  • celebrating indicates whether the celebration has started, initially set to false.

Constants

  const totalCandles: number = 5
const totalBalloons: number = 5
  • totalCandles is a variable that represents the total number of candles, set to 5.
  • totalBalloons is a variable that represents the total number of balloons, also set to 5.

useEffect Hooks

 useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])



useEffect(() => {
if (candlesLit === totalCandles && balloonsPoppedCount === totalBalloons) {
setShowConfetti(true)
}
}, [candlesLit, balloonsPoppedCount])
  • The first useEffect sets up a function to update the windowSize state with the current width and height of the window whenever it is resized, and cleans up the event listener when the component is unmounted.
  • The second useEffect checks if all the candles are lit and all the balloons are popped, and if both conditions are met, it triggers the display of confetti by setting showConfetti to true.
  • Both effects ensure that the component responds dynamically to window resizing and specific conditions related to candles and balloons.

Birthday Functions

  const lightCandle = (index: number) => {
if (index === candlesLit) {
setCandlesLit(prev => prev + 1)
}
}


const popBalloon = (index: number) => {
if (index === balloonsPoppedCount) {
setBalloonsPoppedCount(prev => prev + 1)
}
}


const celebrate = () => {
setCelebrating(true)
setShowConfetti(true)
const interval = setInterval(() => {
setCandlesLit(prev => {
if (prev < totalCandles) return prev + 1
clearInterval(interval)
return prev
})
}, 500)
}
  • The lightCandle function lights the next candle in sequence by increasing the candlesLit count if the current index matches the number of candles already lit.
  • The popBalloon function pops the next balloon in sequence by increasing the balloonsPoppedCount if the current index matches the number of balloons already popped.
  • The celebrate function starts the celebration by setting the celebrating state to true, showing confetti, and lighting all the candles one by one in 500ms intervals until all are lit.

JSX Return Statement

 return (
<div className="min-h-screen bg-white flex items-center justify-center p-4">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="w-full max-w-md"
>
<Card className="mx-auto overflow-hidden transition-all duration-300 ease-in-out hover:shadow-xl border-2 border-black">
<CardHeader className="text-center">
<CardTitle className="text-4xl font-bold text-black">Happy 20th Birthday!</CardTitle>
<CardDescription className="text-2xl font-semibold text-gray-600">Asharib Ali</CardDescription>
<p className="text-lg text-gray-500">September 4th</p>
</CardHeader>
<CardContent className="space-y-6 text-center">
<div>
<h3 className="text-lg font-semibold text-black mb-2">Light the candles:</h3>
<div className="flex justify-center space-x-2">
{[...Array(totalCandles)].map((_, index) => (
<AnimatePresence key={index}>
{(celebrating && index <= candlesLit) || (!celebrating && index < candlesLit) ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
transition={{ duration: 0.5, delay: celebrating ? index * 0.5 : 0 }}
>
<FaBirthdayCake
className={`w-8 h-8 transition-colors duration-300 ease-in-out cursor-pointer hover:scale-110`}
style={{ color: candleColors[index % candleColors.length] }}
onClick={() => lightCandle(index)}
/>
</motion.div>
) : (
// Unlit candle
<FaBirthdayCake
className={`w-8 h-8 text-gray-300 transition-colors duration-300 ease-in-out cursor-pointer hover:scale-110`}
onClick={() => lightCandle(index)}
/>
)}
</AnimatePresence>
))}
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-black mb-2">Pop the balloons:</h3>
<div className="flex justify-center space-x-2">
{[...Array(totalBalloons)].map((_, index) => (
<motion.div
key={index}
initial={{ scale: 1 }}
animate={{ scale: index < balloonsPoppedCount ? 0 : 1 }}
transition={{ duration: 0.3 }}
>
<GiBalloons
className={`w-8 h-8 cursor-pointer hover:scale-110`}
style={{ color: index < balloonsPoppedCount ? '#D1D5DB' : balloonColors[index % balloonColors.length] }}
onClick={() => popBalloon(index)}
/>
</motion.div>
))}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-center">
<Button
className="bg-black text-white hover:bg-gray-800 transition-all duration-300"
onClick={celebrate}
disabled={celebrating}
>
Celebrate! <FaGift className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
</motion.div>
{showConfetti && (
<DynamicConfetti
width={windowSize.width}
height={windowSize.height}
recycle={false}
numberOfPieces={500}
colors={confettiColors}
/>
)}
</div>
)
}
  • The code returns a UI for a birthday celebration card, with a layout that includes a central container and an animated card element that scales and fades in when rendered.
  • The card features sections where users can interactively light candles and pop balloons, with each candle and balloon rendered based on their respective state, using animations to show the lighting and popping effects.
  • A “Celebrate!” button triggers a celebration sequence, including showing confetti across the screen, which is dynamically rendered based on the current window size and includes a variety of colors.

(Bonus just for you): Full Code with Comments

'use client' // Enables client-side rendering for this component

// Import necessary dependencies
import React, { useState, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
import { motion, AnimatePresence } from 'framer-motion'
import dynamic from 'next/dynamic'
import { FaBirthdayCake, FaGift } from 'react-icons/fa'
import { GiBalloons } from 'react-icons/gi'

// Define type for Confetti component props
type ConfettiProps = {
width: number
height: number
}

// Dynamically import Confetti component
const DynamicConfetti = dynamic(() => import('react-confetti'), { ssr: false })

// Define color arrays for candles, balloons, and confetti
const candleColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
const balloonColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
const confettiColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE']

export default function BirthdayWish() {
// State variables
const [candlesLit, setCandlesLit] = useState<number>(0) // Number of lit candles
const [balloonsPoppedCount, setBalloonsPoppedCount] = useState<number>(0) // Number of popped balloons
const [showConfetti, setShowConfetti] = useState<boolean>(false) // Whether to show confetti
const [windowSize, setWindowSize] = useState<ConfettiProps>({ width: 0, height: 0 }) // Window size for confetti
const [celebrating, setCelebrating] = useState<boolean>(false) // Whether celebration has started

// Constants
const totalCandles: number = 5 // Total number of candles
const totalBalloons: number = 5 // Total number of balloons

// Effect to handle window resize
useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])

// Effect to show confetti when all candles are lit and balloons are popped
useEffect(() => {
if (candlesLit === totalCandles && balloonsPoppedCount === totalBalloons) {
setShowConfetti(true)
}
}, [candlesLit, balloonsPoppedCount])

// Function to light a candle
const lightCandle = (index: number) => {
if (index === candlesLit) {
setCandlesLit(prev => prev + 1)
}
}

// Function to pop a balloon
const popBalloon = (index: number) => {
if (index === balloonsPoppedCount) {
setBalloonsPoppedCount(prev => prev + 1)
}
}

// Function to start celebration
const celebrate = () => {
setCelebrating(true)
setShowConfetti(true)
const interval = setInterval(() => {
setCandlesLit(prev => {
if (prev < totalCandles) return prev + 1
clearInterval(interval)
return prev
})
}, 500)
}

return (
// Main container
<div className="min-h-screen bg-white flex items-center justify-center p-4">
{/* Animated wrapper for the card */}
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="w-full max-w-md"
>
{/* Birthday card */}
<Card className="mx-auto overflow-hidden transition-all duration-300 ease-in-out hover:shadow-xl border-2 border-black">
{/* Card header with birthday message */}
<CardHeader className="text-center">
<CardTitle className="text-4xl font-bold text-black">Happy 20th Birthday!</CardTitle>
<CardDescription className="text-2xl font-semibold text-gray-600">Asharib Ali</CardDescription>
<p className="text-lg text-gray-500">September 4th</p>
</CardHeader>
{/* Card content with candles and balloons */}
<CardContent className="space-y-6 text-center">
{/* Candles section */}
<div>
<h3 className="text-lg font-semibold text-black mb-2">Light the candles:</h3>
<div className="flex justify-center space-x-2">
{/* Map through candles */}
{[...Array(totalCandles)].map((_, index) => (
<AnimatePresence key={index}>
{/* Render lit or unlit candle based on state */}
{(celebrating && index <= candlesLit) || (!celebrating && index < candlesLit) ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
transition={{ duration: 0.5, delay: celebrating ? index * 0.5 : 0 }}
>
{/* Lit candle */}
<FaBirthdayCake
className={`w-8 h-8 transition-colors duration-300 ease-in-out cursor-pointer hover:scale-110`}
style={{ color: candleColors[index % candleColors.length] }}
onClick={() => lightCandle(index)}
/>
</motion.div>
) : (
// Unlit candle
<FaBirthdayCake
className={`w-8 h-8 text-gray-300 transition-colors duration-300 ease-in-out cursor-pointer hover:scale-110`}
onClick={() => lightCandle(index)}
/>
)}
</AnimatePresence>
))}
</div>
</div>
{/* Balloons section */}
<div>
<h3 className="text-lg font-semibold text-black mb-2">Pop the balloons:</h3>
<div className="flex justify-center space-x-2">
{/* Map through balloons */}
{[...Array(totalBalloons)].map((_, index) => (
<motion.div
key={index}
initial={{ scale: 1 }}
animate={{ scale: index < balloonsPoppedCount ? 0 : 1 }}
transition={{ duration: 0.3 }}
>
{/* Balloon icon */}
<GiBalloons
className={`w-8 h-8 cursor-pointer hover:scale-110`}
style={{ color: index < balloonsPoppedCount ? '#D1D5DB' : balloonColors[index % balloonColors.length] }}
onClick={() => popBalloon(index)}
/>
</motion.div>
))}
</div>
</div>
</CardContent>
{/* Card footer with celebrate button */}
<CardFooter className="flex justify-center">
<Button
className="bg-black text-white hover:bg-gray-800 transition-all duration-300"
onClick={celebrate}
disabled={celebrating}
>
Celebrate! <FaGift className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
</motion.div>
{/* Confetti component */}
{showConfetti && (
<DynamicConfetti
width={windowSize.width}
height={windowSize.height}
recycle={false}
numberOfPieces={500}
colors={confettiColors}
/>
)}
</div>
)
}

Okay, you’ve completed the main component with functional UI. Now, you need to import this component into the app directory to use it in the app/page.tsx file. Your final code should look like this:

import BirthdayWish from "@/components/birthday-wish";

export default function Home() {
return (
<div>
<BirthdayWish />
</div>
);
}

Running the Project

To see the birthday wish in action, follow these steps:

  1. Start the Development Server: Run npm run dev to start the development server.
  2. Open in Browser: Open http://localhost:3000 in your browser to view the application.

Make sure to test it properly (each and everything) so that we don’t have any errors in the production mode aka when we host on the internet.

Now, we want people to see our application on the internet. All you have to do is create a repository on GitHub and then push your code to it. After that, deploy the Birthday Wish application using Vercel.

Once you’re done with the deployment, please share the application link with me by commenting on this blog post, on Linkedin, and (most importantly, on X a.k.a. Twitter. Tag me there, and I’ll reply and appreciate your efforts) 👀

(Optional): One thing you can do on your own is to add new functionalities, enhance the styling, and improve the overall application. This way, you’ll learn something new by making modifications.

✨ Star Github Repository of the project 👈

Conclusion

In this blog post, we explored the creation of a birthday wish application using Next.js. We covered:

  • The purpose and main features of the application.
  • A detailed breakdown of thebirthday-wish.tsx component, explaining how each part of the code works together.

See you tomorrow with the latest project. Happy coding!

Stay updated with the latest in cutting-edge technology! Follow me:

Thanks for reading!

--

--