Skip to content

Commit 78b4e93

Browse files
committed
add Diwali FestiveModal
1 parent fa2123b commit 78b4e93

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

app/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "./globals.css";
44
import { ThemeProvider } from "@/components/theme-provider";
55
import { Analytics } from "@vercel/analytics/next";
66
import { SITE_URL } from "@/lib/constants";
7+
import { FestiveModal } from "@/components/FestiveModal";
78

89
const geistSans = Geist({
910
variable: "--font-geist-sans",
@@ -74,6 +75,10 @@ export default function RootLayout({
7475
disableTransitionOnChange
7576
>
7677
{children}
78+
<FestiveModal
79+
storageKey="diwali_2025_modal"
80+
expireDate="2025-10-23"
81+
/>
7782
<Analytics />
7883
</ThemeProvider>
7984
</body>

components/FestiveModal.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { X } from "lucide-react";
5+
import { motion, AnimatePresence } from "framer-motion";
6+
7+
interface FestiveModalProps {
8+
storageKey: string;
9+
title?: string;
10+
description?: string;
11+
dismissible?: boolean;
12+
expireDate?: string;
13+
}
14+
15+
export const FestiveModal: React.FC<FestiveModalProps> = ({
16+
storageKey,
17+
title = "🪔 Happy Diwali!",
18+
description = "Wishing you a joyous Diwali filled with lights, happiness, and prosperity.",
19+
dismissible = true,
20+
expireDate,
21+
}) => {
22+
const [visible, setVisible] = useState(false);
23+
24+
useEffect(() => {
25+
if (typeof window === "undefined") return;
26+
27+
const now = new Date();
28+
if (expireDate && now > new Date(expireDate)) {
29+
return;
30+
}
31+
32+
const hasSeen = localStorage.getItem(storageKey);
33+
if (!hasSeen) {
34+
setVisible(true);
35+
localStorage.setItem(storageKey, "true");
36+
}
37+
}, [storageKey, expireDate]);
38+
39+
if (!visible) return null;
40+
41+
return (
42+
<AnimatePresence>
43+
<motion.div
44+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"
45+
initial={{ opacity: 0 }}
46+
animate={{ opacity: 1 }}
47+
exit={{ opacity: 0 }}
48+
transition={{ duration: 0.5 }}
49+
>
50+
<motion.div
51+
className="relative w-full max-w-lg rounded-2xl bg-background shadow-2xl overflow-hidden"
52+
initial={{ scale: 0.8 }}
53+
animate={{ scale: 1 }}
54+
exit={{ scale: 0.8 }}
55+
transition={{ duration: 0.5 }}
56+
>
57+
{dismissible && (
58+
<button
59+
onClick={() => setVisible(false)}
60+
className="absolute top-3 right-3 p-2 rounded-full text-muted-foreground hover:bg-muted/20 transition z-50"
61+
aria-label="Close"
62+
>
63+
<X className="h-5 w-5" />
64+
</button>
65+
)}
66+
67+
<div className="relative p-8 flex flex-col items-center justify-center gap-4 text-center">
68+
<h2 className="text-3xl font-bold text-foreground">{title}</h2>
69+
<p className="text-muted-foreground">{description}</p>
70+
71+
{/* Crackers Animation */}
72+
<div className="absolute inset-0 pointer-events-none overflow-hidden">
73+
{Array.from({ length: 6 }).map((_, i) => (
74+
<motion.span
75+
key={i}
76+
className={`absolute w-2 h-2 bg-orange-400 rounded-full`}
77+
style={{
78+
left: `${10 + i * 15}%`,
79+
top: "90%",
80+
}}
81+
initial={{ opacity: 0, y: 0, scale: 0.5 }}
82+
animate={{ opacity: 1, y: -200, scale: 0.5 }}
83+
transition={{
84+
delay: i * 0.3,
85+
duration: 2,
86+
ease: "easeInOut",
87+
repeat: Infinity,
88+
repeatType: "loop",
89+
}}
90+
/>
91+
))}
92+
</div>
93+
</div>
94+
</motion.div>
95+
</motion.div>
96+
</AnimatePresence>
97+
);
98+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"axios": "^1.12.2",
2222
"class-variance-authority": "^0.7.1",
2323
"clsx": "^2.1.1",
24+
"framer-motion": "^12.23.24",
2425
"lucide-react": "^0.544.0",
2526
"next": "15.5.4",
2627
"next-themes": "^0.4.6",

pnpm-lock.yaml

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)