@titas_mallick/wedding-site-gen 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +70 -184
  2. package/app/api/email-reminders/route.ts +240 -0
  3. package/app/couple/page.tsx +4 -4
  4. package/app/game/page.tsx +298 -0
  5. package/app/guestbook/page.tsx +270 -152
  6. package/app/invitation/[slug]/layout.tsx +4 -2
  7. package/app/invitation/[slug]/page.tsx +303 -84
  8. package/app/invitation/actions.ts +49 -0
  9. package/app/invitation/maker/auth.js +1 -1
  10. package/app/invitation/maker/guestAdder.js +4 -0
  11. package/app/invitation/maker/guestShower.js +39 -8
  12. package/app/invitation/maker/layout.tsx +1 -1
  13. package/app/invitation/maker/page.js +9 -7
  14. package/app/invitation/maker/rsvpViewer.js +90 -8
  15. package/app/layout.tsx +40 -14
  16. package/app/mark-the-dates/page.tsx +8 -2
  17. package/app/page.tsx +7 -1
  18. package/app/providers.tsx +1 -1
  19. package/app/sagun/page.tsx +224 -76
  20. package/app/song-requests/page.tsx +242 -105
  21. package/app/sukanya/page.tsx +9 -13
  22. package/app/titas/page.tsx +8 -24
  23. package/app/travel-guide/page.tsx +361 -120
  24. package/app/updates/maker/page.js +2 -2
  25. package/app/updates/overlay/page.tsx +65 -30
  26. package/app/updates/page.js +3 -3
  27. package/cli.mjs +26 -15
  28. package/components/AdminAuth.tsx +145 -0
  29. package/components/AdminLinks.tsx +120 -0
  30. package/components/ConciergeBot.tsx +104 -44
  31. package/components/CountdownTimer.tsx +37 -15
  32. package/components/Gallery.tsx +1 -1
  33. package/components/LiveVideos.tsx +27 -15
  34. package/components/OurStory.tsx +1 -1
  35. package/components/SchemaMarkup.tsx +74 -0
  36. package/components/certificate.jsx +287 -300
  37. package/components/footer.tsx +2 -0
  38. package/components/hero.tsx +47 -4
  39. package/components/icons.tsx +45 -0
  40. package/components/importantNews.js +168 -168
  41. package/components/navbar.tsx +113 -18
  42. package/components/updates.tsx +36 -26
  43. package/config/firebase-admin.js +14 -17
  44. package/config/firebase.ts +4 -2
  45. package/config/site.ts +10 -2
  46. package/firestore.rules +6 -1
  47. package/next-sitemap.config.js +21 -0
  48. package/package.json +4 -3
  49. package/public/corner1-01.svg +0 -0
  50. package/public/love-birds.png +0 -0
  51. package/public/next.svg +0 -0
  52. package/public/pubqr.png +0 -0
  53. package/public/pw/sample.jpg +0 -0
  54. package/public/qr.png +0 -0
  55. package/public/sample.jpg +0 -0
  56. package/public/vercel.svg +0 -0
  57. package/vercel.json +1 -0
  58. package/.recover +0 -9
  59. package/next-env.d.ts +0 -6
  60. package/public/DCV.gif +0 -0
  61. package/public/DCV2.gif +0 -0
  62. package/public/DCV3.gif +0 -0
  63. package/public/Images/1.jpg +0 -0
  64. package/public/Images/11.jpg +0 -0
  65. package/public/Images/12.jpg +0 -0
  66. package/public/Images/13.jpg +0 -0
  67. package/public/Images/14.jpg +0 -0
  68. package/public/Images/15.jpg +0 -0
  69. package/public/Images/16.jpg +0 -0
  70. package/public/Images/17.jpg +0 -0
  71. package/public/Images/18.jpg +0 -0
  72. package/public/Images/19.jpg +0 -0
  73. package/public/Images/2.jpg +0 -0
  74. package/public/Images/21.jpg +0 -0
  75. package/public/Images/22.jpg +0 -0
  76. package/public/Images/3.jpg +0 -0
  77. package/public/Images/4.jpg +0 -0
  78. package/public/Images/5.jpg +0 -0
  79. package/public/Images/6.jpg +0 -0
  80. package/public/Images/7.jpg +0 -0
  81. package/public/Images/8.jpg +0 -0
  82. package/public/Images/9.jpg +0 -0
  83. package/public/Images/9b.jpg +0 -0
  84. package/public/Images/Patipatra.jpeg +0 -0
  85. package/public/audio (1).mp3 +0 -0
  86. package/public/audio (2).mp3 +0 -0
  87. package/public/bride.jpg +0 -0
  88. package/public/groom.jpg +0 -0
  89. package/public/invite.png +0 -0
  90. package/public/pw/001.jpg +0 -0
  91. package/public/pw/002.jpg +0 -0
  92. package/public/pw/003.jpg +0 -0
  93. package/public/pw/004.jpg +0 -0
  94. package/public/pw/005.jpg +0 -0
  95. package/public/pw/006.jpg +0 -0
  96. package/public/pw/007.jpg +0 -0
  97. package/public/pw/008.jpg +0 -0
  98. package/public/pw/009.jpg +0 -0
  99. package/public/pw/010.jpg +0 -0
  100. package/public/pw/011.jpg +0 -0
  101. package/public/pw/012.jpg +0 -0
  102. package/public/pw/013.jpg +0 -0
  103. package/public/pw/014.jpg +0 -0
  104. package/public/pw/015.jpg +0 -0
  105. package/public/pw/016.jpg +0 -0
  106. package/public/pw/017.jpg +0 -0
  107. package/public/pw/018.jpg +0 -0
  108. package/public/pw/019.jpg +0 -0
  109. package/public/pw/020.jpg +0 -0
  110. package/public/pw/021.jpg +0 -0
  111. package/public/pw/022.jpg +0 -0
  112. package/public/pw/023.jpg +0 -0
  113. package/public/pw/024.jpg +0 -0
  114. package/public/pw/025.jpg +0 -0
  115. package/public/pw/026.jpg +0 -0
  116. package/public/pw/027.jpg +0 -0
  117. package/public/pw/028.jpg +0 -0
  118. package/public/pw/029.jpg +0 -0
  119. package/public/pw/030.jpg +0 -0
  120. package/public/pw/031.jpg +0 -0
  121. package/public/pw/032.jpg +0 -0
  122. package/tsconfig.tsbuildinfo +0 -1
  123. /package/public/Images/{20.jpg → sample.jpg} +0 -0
@@ -68,10 +68,10 @@ const UpdateMaker = () => {
68
68
  contents: [{
69
69
  parts: [{
70
70
  text: `Task: Rewrite the following wedding update message to be more elegant, heartwarming, and professional.
71
- Context: This is for Titas and Sukanya's wedding.
71
+ Context: This is for a wedding celebration.
72
72
  Constraint 1: Output ONLY the refined message in PLAIN TEXT.
73
73
  Constraint 2: Do NOT use any Markdown formatting, bolding (**), or symbols.
74
- Constraint 3: Do not include any introductory text, titles (like "For Titas and Sukanya:"), or quotes.
74
+ Constraint 3: Do not include any introductory text, titles (like "For the couple:"), or quotes.
75
75
 
76
76
  Message to rewrite: "${text}"`
77
77
  }]
@@ -10,6 +10,7 @@ import {
10
10
  limit,
11
11
  } from "firebase/firestore";
12
12
  import { motion, AnimatePresence } from "framer-motion";
13
+ import Image from "next/image";
13
14
 
14
15
  import firebaseApp from "@/config/firebase";
15
16
  import { fontCursive, fontSans, fontMono } from "@/config/fonts";
@@ -28,7 +29,7 @@ export default function UpdateOverlay() {
28
29
  const q = query(
29
30
  collection(db, "updates"),
30
31
  orderBy("createdAt", "desc"),
31
- limit(1)
32
+ limit(1),
32
33
  );
33
34
 
34
35
  const unsubscribe = onSnapshot(q, (snapshot) => {
@@ -49,45 +50,70 @@ export default function UpdateOverlay() {
49
50
  return (
50
51
  <div className="fixed inset-0 flex flex-col justify-between p-16 pointer-events-none">
51
52
  {/* Global CSS for OBS Chroma Keying */}
52
- <style jsx global>{`
53
- nav, footer { display: none !important; }
54
- main {
55
- padding: 0 !important;
56
- max-width: none !important;
57
- margin: 0 !important;
58
- background: #00ff00 !important;
53
+ <style>{`
54
+ nav,
55
+ footer {
56
+ display: none !important;
59
57
  }
60
- body {
61
- background: #00ff00 !important;
62
- overflow: hidden;
58
+ main {
59
+ padding: 0 !important;
60
+ max-width: none !important;
61
+ margin: 0 !important;
62
+ background: #00ff00 !important;
63
+ }
64
+ body {
65
+ background: #00ff00 !important;
66
+ overflow: hidden;
63
67
  }
64
68
  `}</style>
65
69
 
66
70
  {/* Top Bar: Clock & Brand */}
67
71
  <div className="flex justify-between items-start">
68
- <motion.div
69
- initial={{ opacity: 0, x: -50 }}
72
+ <motion.div
70
73
  animate={{ opacity: 1, x: 0 }}
71
74
  className="bg-[#18181b] border-2 border-wedding-pink-500 p-4 rounded-2xl"
75
+ initial={{ opacity: 0, x: -50 }}
72
76
  >
73
- <p className={`${fontMono.className} text-5xl font-black text-white tabular-nums tracking-tighter`}>
74
- {hasMounted ? time.toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "--:--:--"}
77
+ <p
78
+ className={`${fontMono.className} text-5xl font-black text-white tabular-nums tracking-tighter`}
79
+ >
80
+ {hasMounted
81
+ ? time.toLocaleTimeString("en-IN", {
82
+ hour: "2-digit",
83
+ minute: "2-digit",
84
+ second: "2-digit",
85
+ })
86
+ : "--:--:--"}
75
87
  </p>
76
- <p className={`${fontMono.className} text-wedding-gold-400 text-xs uppercase tracking-[0.3em] mt-1 font-bold`}>
88
+ <p
89
+ className={`${fontMono.className} text-wedding-gold-400 text-xs uppercase tracking-[0.3em] mt-1 font-bold`}
90
+ >
77
91
  Event Time
78
92
  </p>
79
93
  </motion.div>
80
94
 
81
- <motion.div
82
- initial={{ opacity: 0, x: 50 }}
95
+ <motion.div
83
96
  animate={{ opacity: 1, x: 0 }}
84
97
  className="bg-white p-3 rounded-2xl flex items-center gap-4 border-2 border-wedding-pink-500"
98
+ initial={{ opacity: 0, x: 50 }}
85
99
  >
86
100
  <div className="text-right">
87
- <p className={`${fontCursive.className} text-2xl text-zinc-900`}>Follow Live</p>
88
- <p className={`${fontSans.className} text-[9px] font-black text-wedding-pink-600 uppercase tracking-widest leading-none`}>Scan for Updates</p>
101
+ <p className={`${fontCursive.className} text-2xl text-zinc-900`}>
102
+ Follow Live
103
+ </p>
104
+ <p
105
+ className={`${fontSans.className} text-[9px] font-black text-wedding-pink-600 uppercase tracking-widest leading-none`}
106
+ >
107
+ Scan for Updates
108
+ </p>
89
109
  </div>
90
- <img src="/pubqr.png" alt="QR Code" className="w-16 h-16 rounded-lg" />
110
+ <Image
111
+ alt="QR Code"
112
+ className="w-16 h-16 rounded-lg"
113
+ height={64}
114
+ src="/pubqr.png"
115
+ width={64}
116
+ />
91
117
  </motion.div>
92
118
  </div>
93
119
 
@@ -96,41 +122,50 @@ export default function UpdateOverlay() {
96
122
  {latestUpdate && (
97
123
  <motion.div
98
124
  key={latestUpdate.id}
99
- initial={{ opacity: 0, y: 150 }}
100
125
  animate={{ opacity: 1, y: 0 }}
126
+ className="w-full flex justify-center"
101
127
  exit={{ opacity: 0, y: 100 }}
128
+ initial={{ opacity: 0, y: 150 }}
102
129
  transition={{ type: "spring", damping: 25, stiffness: 120 }}
103
- className="w-full flex justify-center"
104
130
  >
105
131
  <div className="relative w-full max-w-5xl">
106
132
  {/* Solid outline instead of glow */}
107
133
  <div className="absolute -inset-1 border border-wedding-pink-500/20 rounded-[30px]" />
108
-
134
+
109
135
  <div className="relative bg-[#09090b] border-4 border-wedding-pink-500 rounded-[28px] p-6 md:p-8 flex items-center gap-8">
110
-
111
136
  <div className="flex-shrink-0 flex flex-col items-center gap-2">
112
137
  <div className="bg-wedding-pink-500 p-4 rounded-full">
113
138
  <HeartFilledIcon className="w-8 h-8 text-white" />
114
139
  </div>
115
- <span className={`${fontMono.className} text-[10px] uppercase tracking-[0.4em] text-wedding-gold-400 font-black`}>
140
+ <span
141
+ className={`${fontMono.className} text-[10px] uppercase tracking-[0.4em] text-wedding-gold-400 font-black`}
142
+ >
116
143
  LIVE
117
144
  </span>
118
145
  </div>
119
146
 
120
147
  <div className="flex-1 space-y-2">
121
- <p className={`${fontCursive.className} text-4xl text-wedding-pink-400`}>
148
+ <p
149
+ className={`${fontCursive.className} text-4xl text-wedding-pink-400`}
150
+ >
122
151
  Wedding News
123
152
  </p>
124
- <p className={`${fontSans.className} text-3xl md:text-4xl font-black text-white leading-tight tracking-tight`}>
153
+ <p
154
+ className={`${fontSans.className} text-3xl md:text-4xl font-black text-white leading-tight tracking-tight`}
155
+ >
125
156
  {latestUpdate.text}
126
157
  </p>
127
158
  </div>
128
159
 
129
160
  <div className="hidden lg:flex flex-col items-end justify-center border-l-2 border-zinc-800 pl-8 space-y-1">
130
- <p className={`${fontCursive.className} text-4xl text-wedding-gold-200`}>
161
+ <p
162
+ className={`${fontCursive.className} text-4xl text-wedding-gold-200`}
163
+ >
131
164
  Titas & Sukanya
132
165
  </p>
133
- <p className={`${fontMono.className} text-[10px] uppercase tracking-[0.5em] text-zinc-500 font-bold`}>
166
+ <p
167
+ className={`${fontMono.className} text-[10px] uppercase tracking-[0.5em] text-zinc-500 font-bold`}
168
+ >
134
169
  Jan 2026
135
170
  </p>
136
171
  </div>
@@ -158,13 +158,13 @@ const UpdateFeed = () => {
158
158
  color={index === 0 ? "warning" : "danger"}
159
159
  radius="full"
160
160
  size="md"
161
- src={update.email === "titas@titas.titas" ? "/groom.jpg" : undefined}
162
- name={update.email === "titas@titas.titas" ? "T" : update.email?.charAt(0).toUpperCase()}
161
+ src={update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "/groom.jpg" : undefined}
162
+ name={update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "T" : update.email?.charAt(0).toUpperCase()}
163
163
  />
164
164
  <div className="flex flex-col flex-1">
165
165
  <div className="flex items-center justify-between">
166
166
  <p className="text-md font-bold text-default-800 dark:text-white">
167
- {update.email === "titas@titas.titas" ? "Titas Mallick" : update.email}
167
+ {update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "Titas Mallick" : update.email}
168
168
  </p>
169
169
  {index === 0 && (
170
170
  <Chip
package/cli.mjs CHANGED
@@ -17,7 +17,7 @@ const rl = readline.createInterface({
17
17
  const question = (query) => new Promise((resolve) => rl.question(query, resolve));
18
18
 
19
19
  async function main() {
20
- console.log("Welcome to the Wedding Website Generator! 💍");
20
+ console.log("Welcome to the Wedding Website Generator v2.0! 💍");
21
21
  console.log("This tool will help you scaffold a personalized wedding website.\n");
22
22
 
23
23
  const config = {};
@@ -86,21 +86,23 @@ async function main() {
86
86
  ];
87
87
 
88
88
  const replacements = [
89
- { search: /Titas & Sukanya/g, replace: `${config.groomName} & ${config.brideName}` },
90
- { search: /Titas and Sukanya/g, replace: `${config.groomName} and ${config.brideName}` },
91
- { search: /Titas Mallick/g, replace: config.groomFullName },
92
- { search: /Sukanya Saha/g, replace: config.brideFullName },
89
+ { search: /Groom & Bride/g, replace: `${config.groomName} & ${config.brideName}` },
90
+ { search: /Groom and Bride/g, replace: `${config.groomName} and ${config.brideName}` },
91
+ { search: /Groom Name/g, replace: config.groomFullName },
92
+ { search: /Bride Name/g, replace: config.brideFullName },
93
+ { search: /the couple/g, replace: `${config.groomName} and ${config.brideName}` },
94
+ { search: /the groom/g, replace: config.groomName },
93
95
  { search: /#TitasWedsSukanya/g, replace: config.hashtag },
94
- { search: /titas-sukanya-for.life/g, replace: config.siteUrl.replace(/^https?:\/\//, '') },
96
+ { search: /your-wedding-site.com/g, replace: config.siteUrl.replace(/^https?:\/\//, '') },
95
97
  { search: /January 23, 2026/g, replace: config.weddingDate },
96
98
  { search: /2026-01-23/g, replace: config.weddingDateISO },
97
- { search: /titas@titas.titas/g, replace: config.adminEmail },
98
- { search: /eugenics@ybl/g, replace: config.upiId },
99
- // Generic replacements for names
100
- { search: /\bTitas\b/g, replace: config.groomName },
101
- { search: /\btitas\b/g, replace: config.groomName.toLowerCase() },
102
- { search: /\bSukanya\b/g, replace: config.brideName },
103
- { search: /\bsukanya\b/g, replace: config.brideName.toLowerCase() },
99
+ { search: /admin@example.com/g, replace: config.adminEmail },
100
+ { search: /your-upi-id@upi/g, replace: config.upiId },
101
+ // Code-level replacements for exported components/functions
102
+ { search: /TitasLayout/g, replace: `${config.groomName}Layout` },
103
+ { search: /SukanyaLayout/g, replace: `${config.brideName}Layout` },
104
+ { search: /TitasPage/g, replace: `${config.groomName}Page` },
105
+ { search: /SukanyaPage/g, replace: `${config.brideName}Page` },
104
106
  ];
105
107
 
106
108
  async function copyDir(src, dest) {
@@ -123,7 +125,7 @@ const adminCred = {
123
125
  type: "service_account",
124
126
  project_id: process.env.FIREBASE_ADMIN_PROJECT_ID,
125
127
  private_key_id: process.env.FIREBASE_ADMIN_PRIVATE_KEY_ID,
126
- private_key: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\\\n/g, '\\n'),
128
+ private_key: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'),
127
129
  client_email: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
128
130
  client_id: process.env.FIREBASE_ADMIN_CLIENT_ID,
129
131
  auth_uri: "https://accounts.google.com/o/oauth2/auth",
@@ -196,12 +198,21 @@ FIREBASE_ADMIN_CLIENT_EMAIL=
196
198
  FIREBASE_ADMIN_CLIENT_ID=
197
199
  FIREBASE_ADMIN_CLIENT_X509_CERT_URL=
198
200
 
201
+ # Admin Controls
202
+ NEXT_PUBLIC_ADMIN_EMAIL=${config.adminEmail}
203
+
199
204
  # Google Gemini AI
200
205
  NEXT_PUBLIC_GEMINI_API_KEY=
201
206
 
202
- # Cloudinary Config
207
+ # Cloudinary Config (for guestbook uploads)
203
208
  NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
204
209
  NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=wedding
210
+
211
+ # Resend API (for email reminders)
212
+ RESEND_API_KEY=
213
+
214
+ # Cron Security (for reminders endpoint)
215
+ CRON_SECRET=
205
216
  `.trim();
206
217
 
207
218
  await fs.writeFile(path.join(absoluteTargetDir, '.env.local'), envContent);
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import {
5
+ Modal,
6
+ ModalContent,
7
+ ModalHeader,
8
+ ModalBody,
9
+ ModalFooter,
10
+ Button,
11
+ Input,
12
+ addToast,
13
+ } from "@heroui/react";
14
+ import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged, User } from "firebase/auth";
15
+ import firebaseApp from "@/config/firebase";
16
+
17
+ export const AdminAuth = () => {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [user, setUser] = useState<User | null>(null);
20
+ const [email, setEmail] = useState("");
21
+ const [password, setPassword] = useState("");
22
+ const [loading, setLoading] = useState(false);
23
+
24
+ const auth = getAuth(firebaseApp());
25
+
26
+ useEffect(() => {
27
+ const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
28
+ setUser(currentUser);
29
+ });
30
+ return () => unsubscribe();
31
+ }, [auth]);
32
+
33
+ const handleLogin = async (e: React.FormEvent) => {
34
+ e.preventDefault();
35
+ setLoading(true);
36
+ try {
37
+ await signInWithEmailAndPassword(auth, email, password);
38
+ addToast({
39
+ title: "Welcome Titas",
40
+ description: "Admin access granted.",
41
+ color: "success",
42
+ });
43
+ setIsOpen(false);
44
+ setEmail("");
45
+ setPassword("");
46
+ } catch (error) {
47
+ addToast({
48
+ title: "Access Denied",
49
+ description: "Invalid credentials.",
50
+ color: "danger",
51
+ });
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ const handleLogout = async () => {
58
+ try {
59
+ await signOut(auth);
60
+ addToast({
61
+ title: "Signed Out",
62
+ description: "Admin session ended.",
63
+ color: "warning",
64
+ });
65
+ setIsOpen(false);
66
+ } catch (error) {
67
+ console.error("Logout error", error);
68
+ }
69
+ };
70
+
71
+ return (
72
+ <>
73
+ <button
74
+ onClick={() => setIsOpen(true)}
75
+ className="text-[10px] text-default-300 dark:text-default-700 hover:text-wedding-pink-500 transition-colors opacity-30 hover:opacity-100 focus:outline-none"
76
+ aria-label="Admin Access"
77
+ >
78
+
79
+ </button>
80
+
81
+ <Modal isOpen={isOpen} onOpenChange={setIsOpen} backdrop="blur" placement="center">
82
+ <ModalContent>
83
+ {(onClose) => (
84
+ <>
85
+ <ModalHeader className="flex flex-col gap-1">
86
+ {user ? "Admin Profile" : "Secure Login"}
87
+ </ModalHeader>
88
+ <ModalBody>
89
+ {user ? (
90
+ <div className="py-4 text-center">
91
+ <p className="text-default-500 mb-2">Logged in as</p>
92
+ <p className="font-bold text-lg text-wedding-pink-600">{user.email}</p>
93
+ </div>
94
+ ) : (
95
+ <form onSubmit={handleLogin} className="space-y-4">
96
+ <Input
97
+ label="Admin Email"
98
+ labelPlacement="outside-top"
99
+ placeholder="Enter admin email"
100
+ variant="bordered"
101
+ value={email}
102
+ onChange={(e) => setEmail(e.target.value)}
103
+ isRequired
104
+ classNames={{ input: "outline-none" }}
105
+ />
106
+ <Input
107
+ label="Password"
108
+ labelPlacement="outside-top"
109
+ placeholder="Enter password"
110
+ type="password"
111
+ variant="bordered"
112
+ value={password}
113
+ onChange={(e) => setPassword(e.target.value)}
114
+ isRequired
115
+ classNames={{ input: "outline-none" }}
116
+ />
117
+ <Button
118
+ type="submit"
119
+ color="primary"
120
+ className="w-full bg-wedding-pink-500"
121
+ isLoading={loading}
122
+ >
123
+ Login
124
+ </Button>
125
+ </form>
126
+ )}
127
+ </ModalBody>
128
+ <ModalFooter>
129
+ {user ? (
130
+ <Button color="danger" variant="flat" onClick={handleLogout} className="w-full">
131
+ Logout
132
+ </Button>
133
+ ) : (
134
+ <Button variant="light" onClick={onClose} className="w-full">
135
+ Cancel
136
+ </Button>
137
+ )}
138
+ </ModalFooter>
139
+ </>
140
+ )}
141
+ </ModalContent>
142
+ </Modal>
143
+ </>
144
+ );
145
+ };
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { getAuth, onAuthStateChanged, User } from "firebase/auth";
5
+ import { Button } from "@heroui/button";
6
+ import { Link } from "@heroui/link";
7
+ import { motion } from "framer-motion";
8
+
9
+ import firebaseApp from "@/config/firebase";
10
+ import { title } from "@/components/primitives";
11
+
12
+ const ADMIN_EMAIL = process.env.NEXT_PUBLIC_ADMIN_EMAIL;
13
+
14
+ export const AdminLinks = () => {
15
+ const [user, setUser] = useState<User | null>(null);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ const auth = getAuth(firebaseApp());
20
+ const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
21
+ setUser(currentUser);
22
+ setLoading(false);
23
+ });
24
+ return () => unsubscribe();
25
+ }, []);
26
+
27
+ if (loading || !user || user.email !== ADMIN_EMAIL) {
28
+ return null;
29
+ }
30
+
31
+ const adminTools = [
32
+ {
33
+ name: "Invitation Maker",
34
+ description: "Create and manage guest invitations",
35
+ href: "/invitation/maker",
36
+ icon: "✉️",
37
+ color: "gold",
38
+ },
39
+ {
40
+ name: "Updates Maker",
41
+ description: "Post new announcements for guests",
42
+ href: "/updates/maker",
43
+ icon: "📢",
44
+ color: "pink",
45
+ },
46
+ {
47
+ name: "Guestbook",
48
+ description: "Review wishes and messages",
49
+ href: "/guestbook",
50
+ icon: "📖",
51
+ color: "blue",
52
+ },
53
+ ];
54
+
55
+ return (
56
+ <section className="container mx-auto px-4 pt-12">
57
+ <motion.div
58
+ initial={{ opacity: 0, y: -20 }}
59
+ animate={{ opacity: 1, y: 0 }}
60
+ className="relative overflow-hidden rounded-[2.5rem] bg-white/50 dark:bg-zinc-900/50 backdrop-blur-2xl border border-white dark:border-white/10 shadow-2xl"
61
+ >
62
+ {/* Decorative background elements */}
63
+ <div className="absolute top-0 right-0 w-64 h-64 bg-wedding-gold-200/20 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none" />
64
+ <div className="absolute bottom-0 left-0 w-64 h-64 bg-wedding-pink-200/20 rounded-full blur-3xl -ml-20 -mb-20 pointer-events-none" />
65
+
66
+ <div className="relative z-10 p-8 md:p-12">
67
+ <div className="flex flex-col md:flex-row md:items-end justify-between mb-10 gap-4">
68
+ <div>
69
+ <div className="flex items-center gap-3 mb-2">
70
+ <span className="flex h-3 w-3 rounded-full bg-green-500 animate-pulse" />
71
+ <span className="text-xs font-black uppercase tracking-[0.3em] text-default-400">
72
+ Secure Admin Access
73
+ </span>
74
+ </div>
75
+ <h2 className={title({ size: "sm", color: "foreground" })}>
76
+ Control Center&nbsp;
77
+ </h2>
78
+ <p className="text-default-500 mt-2 font-medium">
79
+ Welcome, <span className="text-wedding-pink-500">{user.email}</span>. What would you like to manage today?
80
+ </p>
81
+ </div>
82
+ <div className="bg-default-100 dark:bg-white/5 px-4 py-2 rounded-2xl border border-default-200 dark:border-white/10">
83
+ <p className="text-[10px] uppercase tracking-wider text-default-400 font-bold mb-1">Last Sync</p>
84
+ <p className="text-sm font-mono">{new Date().toLocaleTimeString()}</p>
85
+ </div>
86
+ </div>
87
+
88
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
89
+ {adminTools.map((tool, index) => (
90
+ <motion.div
91
+ key={tool.name}
92
+ initial={{ opacity: 0, y: 20 }}
93
+ animate={{ opacity: 1, y: 0 }}
94
+ transition={{ delay: index * 0.1 }}
95
+ whileHover={{ y: -5 }}
96
+ >
97
+ <Link href={tool.href} className="w-full block">
98
+ <div className="group p-6 rounded-[2rem] bg-white dark:bg-zinc-800/50 border border-default-200 dark:border-white/5 hover:border-wedding-pink-300 dark:hover:border-wedding-pink-900 transition-all duration-300 shadow-sm hover:shadow-xl">
99
+ <div className="flex items-start justify-between mb-4">
100
+ <div className="text-4xl">{tool.icon}</div>
101
+ <div className="p-2 rounded-full bg-default-50 dark:bg-white/5 group-hover:bg-wedding-pink-500 group-hover:text-white transition-colors">
102
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
103
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
104
+ </svg>
105
+ </div>
106
+ </div>
107
+ <h4 className="text-xl font-bold text-default-900 mb-1">{tool.name}</h4>
108
+ <p className="text-sm text-default-500 leading-relaxed">
109
+ {tool.description}
110
+ </p>
111
+ </div>
112
+ </Link>
113
+ </motion.div>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ </motion.div>
118
+ </section>
119
+ );
120
+ };