@titas_mallick/wedding-site-gen 1.1.0 → 2.0.1

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 +104 -170
  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
@@ -1,7 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState, use } from "react";
4
- import { getFirestore, doc, getDoc, addDoc, collection, serverTimestamp } from "firebase/firestore";
4
+ import {
5
+ getFirestore,
6
+ doc,
7
+ getDoc,
8
+ addDoc,
9
+ collection,
10
+ serverTimestamp,
11
+ query,
12
+ where,
13
+ getDocs,
14
+ } from "firebase/firestore";
5
15
  import {
6
16
  Card,
7
17
  CardBody,
@@ -22,7 +32,7 @@ import { motion, AnimatePresence } from "framer-motion";
22
32
 
23
33
  import { fontCursive, fontSans, fontMono } from "@/config/fonts";
24
34
  import firebaseApp from "@/config/firebase";
25
- import { MapPinIcon, CalendarIcon, ClockIcon } from "@/components/icons";
35
+ import { MapPinIcon, CalendarIcon, ClockIcon, CheckIcon } from "@/components/icons";
26
36
 
27
37
  export default function InvitationPage({
28
38
  params,
@@ -36,22 +46,35 @@ export default function InvitationPage({
36
46
  const [notFound, setNotFound] = useState(false);
37
47
  const [isTranslating, setIsTranslating] = useState(false);
38
48
  const [translatedData, setTranslatedData] = useState<any>(null);
39
-
49
+
40
50
  // RSVP States
41
51
  const [showRSVP, setShowRSVP] = useState(false);
42
- const [rsvpData, setRsvpData] = useState({ attending: "yes", guests: 1, food: "non-veg", note: "", captcha: "" });
52
+ const [rsvpData, setRsvpData] = useState({
53
+ attending: "yes",
54
+ guests: 1,
55
+ food: "non-veg",
56
+ note: "",
57
+ captcha: "",
58
+ });
43
59
  const [rsvpLoading, setRsvpLoading] = useState(false);
44
60
  const [hasRSVPed, setHasRSVPed] = useState(false);
61
+ const [hasEmailReminder, setHasEmailReminder] = useState(false);
45
62
 
46
63
  const handleRSVP = async () => {
47
64
  if (rsvpData.captcha.trim() !== "10") {
48
- addToast({ title: "Security Check", description: "Please answer correctly (Hint: 10).", color: "warning" });
65
+ addToast({
66
+ title: "Security Check",
67
+ description: "Please answer correctly (Hint: 10).",
68
+ color: "warning",
69
+ });
70
+
49
71
  return;
50
72
  }
51
73
 
52
74
  setRsvpLoading(true);
53
75
  try {
54
76
  const db = getFirestore(firebaseApp());
77
+
55
78
  await addDoc(collection(db, "rsvps"), {
56
79
  guestId: slug,
57
80
  guestName: guest.name,
@@ -59,17 +82,61 @@ export default function InvitationPage({
59
82
  timestamp: serverTimestamp(),
60
83
  });
61
84
  setHasRSVPed(true);
62
- addToast({ title: "RSVP Received", description: "Thank you for confirming!", color: "success" });
85
+ addToast({
86
+ title: "RSVP Received",
87
+ description: "Thank you for confirming!",
88
+ color: "success",
89
+ });
63
90
  } catch (err) {
64
91
  console.error("RSVP error", err);
65
- addToast({ title: "Error", description: "Failed to send RSVP. Please try again.", color: "danger" });
92
+ addToast({
93
+ title: "Error",
94
+ description: "Failed to send RSVP. Please try again.",
95
+ color: "danger",
96
+ });
66
97
  } finally {
67
98
  setRsvpLoading(false);
68
99
  }
69
100
  };
70
101
 
71
102
  const translateContent = async () => {
72
- // ... rest of translation logic remains same
103
+ if (translatedData) {
104
+ setTranslatedData(null);
105
+
106
+ return;
107
+ }
108
+
109
+ if (!guest) return;
110
+
111
+ setIsTranslating(true);
112
+
113
+ try {
114
+ // Simulate network delay for effect
115
+ await new Promise((resolve) => setTimeout(resolve, 600));
116
+
117
+ const relationMap: Record<string, string> = {
118
+ family: "আত্মীয়",
119
+ friend: "বন্ধু",
120
+ colleague: "সহকর্মী",
121
+ other: "শুভাকাঙ্ক্ষী",
122
+ };
123
+
124
+ setTranslatedData({
125
+ guestName: guest.name || "অতিথি",
126
+ familySide: guest.familySide,
127
+ relation: relationMap[guest.relation?.toLowerCase()] || guest.relation || "শুভাকাঙ্ক্ষী",
128
+ });
129
+ } catch (error) {
130
+ console.error("Translation error:", error);
131
+ addToast({
132
+ title: "অনুবাদ ব্যর্থ হয়েছে",
133
+ description: "দুঃখিত, অনুবাদ করার সময় একটি সমস্যা হয়েছে।",
134
+ color: "danger",
135
+ });
136
+ setTranslatedData(null);
137
+ } finally {
138
+ setIsTranslating(false);
139
+ }
73
140
  };
74
141
 
75
142
  useEffect(() => {
@@ -81,11 +148,24 @@ export default function InvitationPage({
81
148
 
82
149
  if (docSnap.exists()) {
83
150
  const guestData = docSnap.data();
151
+
84
152
  setGuest(guestData);
153
+
154
+ // Check for existing email reminders
155
+ const remindersQuery = query(
156
+ collection(db, "email-reminders"),
157
+ where("guestId", "==", slug),
158
+ );
159
+ const remindersSnap = await getDocs(remindersQuery);
160
+
161
+ if (!remindersSnap.empty) {
162
+ setHasEmailReminder(true);
163
+ }
85
164
  } else {
86
165
  setNotFound(true);
87
166
  }
88
167
  } catch (error) {
168
+ console.error("Fetch error:", error);
89
169
  setNotFound(true);
90
170
  } finally {
91
171
  setLoading(false);
@@ -97,31 +177,31 @@ export default function InvitationPage({
97
177
 
98
178
  const events: any = {
99
179
  registration: {
100
- title: "Engagement Ceremony",
101
- date: "23rd November 2025",
102
- venue: "Srerampore",
180
+ title: translatedData ? "শুভ আংটি বদল" : "Engagement Ceremony",
181
+ date: translatedData ? "২৩শে নভেম্বর, ২০২৫" : "23rd November 2025",
182
+ venue: translatedData ? "শ্রীরামপুর" : "Srerampore",
103
183
  map: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d229.96354422747754!2d88.34617843336005!3d22.749911773448982!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x39f89b1ee7f14647%3A0xe44dfacd65bb5f48!2sMohan%20Joyti%20Banquet%20Hall!5e0!3m2!1sen!2sin!4v1758679491438!5m2!1sen!2sin",
104
184
  mapHref: "https://maps.app.goo.gl/diSeycey1hyqqtAu8",
105
- time: "10:00 AM",
106
- direction: "Near Battala",
185
+ time: translatedData ? "সকাল ১০:০০টা" : "10:00 AM",
186
+ direction: translatedData ? "বটতলার কাছে" : "Near Battala",
107
187
  },
108
188
  wedding: {
109
- title: "Wedding Ceremony",
110
- date: "23rd January 2026",
111
- venue: "Serampore",
189
+ title: translatedData ? "শুভ বিবাহ" : "Wedding Ceremony",
190
+ date: translatedData ? "২৩শে জানুয়ারি, ২০২৬" : "23rd January 2026",
191
+ venue: translatedData ? "শ্রীরামপুর" : "Serampore",
112
192
  map: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3679.3779465759876!2d88.33077587399126!3d22.75135112639763!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x39f89b287ddfb39b%3A0xc67348083f7cea9d!2sAnandamayee%20Bhawan!5e0!3m2!1sen!2sin!4v1765848206729!5m2!1sen!2sin",
113
193
  mapHref: "https://maps.app.goo.gl/G5R3bkcTwwa2d54R8",
114
- time: "06:00 PM",
115
- direction: "Near Belting Bazar",
194
+ time: translatedData ? "সন্ধ্যা ০৬:০০টা" : "06:00 PM",
195
+ direction: translatedData ? "বেল্টিং বাজারের কাছে" : "Near Belting Bazar",
116
196
  },
117
197
  reception: {
118
- title: "Reception Celebration",
119
- date: "25th January 2026",
120
- venue: "Konnagar",
198
+ title: translatedData ? "প্রীতিভোজ" : "Reception Celebration",
199
+ date: translatedData ? "২৫শে জানুয়ারি, ২০২৬" : "25th January 2026",
200
+ venue: translatedData ? "কোন্নগর" : "Konnagar",
121
201
  map: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3680.8284397695707!2d88.35295167398938!3d22.697429628386114!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x39f89c85a91d38fd%3A0x19dcd2c61895f79f!2sKonnagar%20Friends%20Union%20Club%20Community%20Centre%2FGangadhar%20Chatterjee%20Bhaban!5e0!3m2!1sen!2sin!4v1765848530552!5m2!1sen!2sin",
122
202
  mapHref: "https://maps.app.goo.gl/ubdTsy6tnYMsvSSXA",
123
- time: "06:00 PM",
124
- direction: "Near GT Road",
203
+ time: translatedData ? "সন্ধ্যা ০৬:০০টা" : "06:00 PM",
204
+ direction: translatedData ? "জি.টি. রোডের কাছে" : "Near GT Road",
125
205
  },
126
206
  };
127
207
 
@@ -136,7 +216,7 @@ export default function InvitationPage({
136
216
  <p className="text-default-600 dark:text-gray-300">{event.date}</p>
137
217
  </div>
138
218
  </div>
139
-
219
+
140
220
  <div className="flex items-start gap-4">
141
221
  <ClockIcon className="w-6 h-6 text-wedding-gold-500 mt-1" />
142
222
  <div>
@@ -149,16 +229,18 @@ export default function InvitationPage({
149
229
  <MapPinIcon className="w-6 h-6 text-indigo-500 mt-1" />
150
230
  <div>
151
231
  <p className="font-bold text-default-800 dark:text-white">Venue</p>
152
- <p className="text-default-600 dark:text-gray-300 font-medium">{event.venue}</p>
232
+ <p className="text-default-600 dark:text-gray-300 font-medium">
233
+ {event.venue}
234
+ </p>
153
235
  <p className="text-default-500 text-sm mt-1">{event.direction}</p>
154
236
  </div>
155
237
  </div>
156
238
 
157
239
  <Button
158
- as={Link}
159
- href={event.mapHref}
160
240
  isExternal
241
+ as={Link}
161
242
  className="w-full bg-wedding-pink-100 text-wedding-pink-700 font-semibold hover:bg-wedding-pink-200 dark:bg-wedding-pink-900/30 dark:text-wedding-pink-200 mt-4"
243
+ href={event.mapHref}
162
244
  startContent={<MapPinIcon className="w-4 h-4" />}
163
245
  >
164
246
  Open in Google Maps
@@ -168,12 +250,12 @@ export default function InvitationPage({
168
250
  {/* Map Embed */}
169
251
  <div className="h-64 md:h-auto rounded-2xl overflow-hidden shadow-lg border border-default-200 dark:border-default-700">
170
252
  <iframe
171
- title={`${event.title} Map`}
253
+ allowFullScreen
172
254
  className="w-full h-full"
173
- src={event.map}
174
255
  loading="lazy"
175
- allowFullScreen
176
- ></iframe>
256
+ src={event.map}
257
+ title={`${event.title} Map`}
258
+ />
177
259
  </div>
178
260
  </div>
179
261
  );
@@ -218,7 +300,7 @@ export default function InvitationPage({
218
300
  <p
219
301
  className={`${fontCursive.className} text-4xl md:text-7xl font-bold drop-shadow-md px-2`}
220
302
  >
221
- Titas & Sukanya
303
+ Groom & Bride
222
304
  </p>
223
305
  <p
224
306
  className={`${fontMono.className} mt-2 md:mt-4 text-[10px] md:text-base uppercase tracking-widest opacity-90 px-4`}
@@ -228,11 +310,11 @@ export default function InvitationPage({
228
310
  </div>
229
311
  {/* Translation Button */}
230
312
  <Button
313
+ className="absolute bottom-2 right-2 bg-white/20 text-white backdrop-blur-md border border-white/30 text-[10px] h-7 px-2"
314
+ isLoading={isTranslating}
231
315
  size="sm"
232
316
  variant="flat"
233
317
  onPress={translateContent}
234
- isLoading={isTranslating}
235
- className="absolute bottom-2 right-2 bg-white/20 text-white backdrop-blur-md border border-white/30 text-[10px] h-7 px-2"
236
318
  >
237
319
  {translatedData ? "See English" : "বাংলায় দেখুন"}
238
320
  </Button>
@@ -244,9 +326,12 @@ export default function InvitationPage({
244
326
  <p
245
327
  className={`${fontCursive.className} text-3xl md:text-5xl text-wedding-pink-600 dark:text-wedding-pink-400 mb-4 md:mb-6`}
246
328
  >
247
- {translatedData ? "প্রিয়" : "Dear"} {translatedData?.guestName || guest.name}
329
+ {translatedData ? "প্রিয়" : "Dear"}{" "}
330
+ {translatedData?.guestName || guest.name}
248
331
  {guest.invitedGuests > 1 && (
249
- <span className="text-xl md:text-3xl ml-2">{translatedData ? "& পরিবার" : "& Family"}</span>
332
+ <span className="text-xl md:text-3xl ml-2">
333
+ {translatedData ? "& পরিবার" : "& Family"}
334
+ </span>
250
335
  )}
251
336
  ,
252
337
  </p>
@@ -257,7 +342,8 @@ export default function InvitationPage({
257
342
  <p>
258
343
  {translatedData ? (
259
344
  <>
260
- {translatedData.welcomeNote || `${translatedData.familySide === "bride" ? "কনে" : "বর"}-র পক্ষ থেকে একজন প্রিয় ${translatedData.relation} হিসেবে আপনার উপস্থিতি আমাদের কাছে অনেক গুরুত্বপূর্ণ।`}
345
+ {translatedData.welcomeNote ||
346
+ `${translatedData.familySide === "bride" ? "কনে" : "বর"}-র পক্ষ থেকে একজন প্রিয় ${translatedData.relation} হিসেবে আপনার উপস্থিতি আমাদের কাছে অনেক গুরুত্বপূর্ণ।`}
261
347
  </>
262
348
  ) : (
263
349
  <>
@@ -271,7 +357,9 @@ export default function InvitationPage({
271
357
  )}
272
358
  </p>
273
359
  <p>
274
- {translatedData ? "আমরা আপনাকে নিম্নলিখিত অনুষ্ঠানে যোগ দেওয়ার জন্য সাদর আমন্ত্রণ জানাচ্ছি:" : `We warmly invite you to join us for the following celebration${guest.invitedFor.length > 1 ? "s" : ""}:`}
360
+ {translatedData
361
+ ? "আমরা আপনাকে নিম্নলিখিত অনুষ্ঠানে যোগ দেওয়ার জন্য সাদর আমন্ত্রণ জানাচ্ছি:"
362
+ : `We warmly invite you to join us for the following celebration${guest.invitedFor.length > 1 ? "s" : ""}:`}
275
363
  </p>
276
364
  </div>
277
365
  </div>
@@ -282,14 +370,16 @@ export default function InvitationPage({
282
370
  <div className="hidden md:block">
283
371
  <Tabs
284
372
  aria-label="Wedding Events"
285
- color="danger"
286
- variant="underlined"
287
373
  classNames={{
288
- tabList: "gap-6 w-full relative rounded-none p-0 border-b border-divider",
374
+ tabList:
375
+ "gap-6 w-full relative rounded-none p-0 border-b border-divider",
289
376
  cursor: "w-full bg-wedding-pink-500",
290
377
  tab: "max-w-fit px-0 h-12",
291
- tabContent: "group-data-[selected=true]:text-wedding-pink-500 font-medium text-lg"
378
+ tabContent:
379
+ "group-data-[selected=true]:text-wedding-pink-500 font-medium text-lg",
292
380
  }}
381
+ color="danger"
382
+ variant="underlined"
293
383
  >
294
384
  {guest.invitedFor.map((eventKey: string) => (
295
385
  <Tab key={eventKey} title={events[eventKey].title}>
@@ -301,22 +391,22 @@ export default function InvitationPage({
301
391
 
302
392
  {/* Mobile Accordion */}
303
393
  <div className="md:hidden text-left">
304
- <Accordion
305
- variant="splitted"
306
- selectionMode="multiple"
307
- defaultExpandedKeys={guest.invitedFor}
394
+ <Accordion
308
395
  className="px-0"
396
+ defaultExpandedKeys={guest.invitedFor}
397
+ selectionMode="multiple"
398
+ variant="splitted"
309
399
  >
310
400
  {guest.invitedFor.map((eventKey: string) => (
311
401
  <AccordionItem
312
402
  key={eventKey}
313
403
  aria-label={events[eventKey].title}
404
+ className="bg-default-50 dark:bg-default-100/5 border border-default-100 dark:border-default-800 rounded-xl shadow-sm mb-4"
314
405
  title={
315
406
  <span className="font-bold text-wedding-pink-600 dark:text-wedding-pink-400">
316
407
  {events[eventKey].title}
317
408
  </span>
318
409
  }
319
- className="bg-default-50 dark:bg-default-100/5 border border-default-100 dark:border-default-800 rounded-xl shadow-sm mb-4"
320
410
  >
321
411
  {renderEventDetails(events[eventKey])}
322
412
  </AccordionItem>
@@ -332,92 +422,157 @@ export default function InvitationPage({
332
422
  <AnimatePresence mode="wait">
333
423
  {hasRSVPed ? (
334
424
  <motion.div
335
- initial={{ opacity: 0, scale: 0.9 }}
336
425
  animate={{ opacity: 1, scale: 1 }}
337
426
  className="bg-green-50 dark:bg-green-900/20 p-6 md:p-8 rounded-3xl border border-green-100 dark:border-green-800/30 text-center"
427
+ initial={{ opacity: 0, scale: 0.9 }}
338
428
  >
339
- <p className={`${fontCursive.className} text-2xl md:text-3xl text-green-600 dark:text-green-400 mb-2`}>Thank You!</p>
340
- <p className="text-sm md:text-green-700 dark:text-green-300 font-medium">We have received your RSVP and can't wait to see you.</p>
429
+ <p
430
+ className={`${fontCursive.className} text-2xl md:text-3xl text-green-600 dark:text-green-400 mb-2`}
431
+ >
432
+ Thank You!
433
+ </p>
434
+ <p className="text-sm md:text-green-700 dark:text-green-300 font-medium">
435
+ We have received your RSVP and can&apos;t wait to see you.
436
+ </p>
341
437
  </motion.div>
342
438
  ) : !showRSVP ? (
343
- <Button
344
- onPress={() => setShowRSVP(true)}
439
+ <Button
345
440
  className="bg-wedding-pink-500 text-white font-bold h-12 md:h-14 px-8 md:px-10 text-base md:text-lg rounded-full shadow-lg shadow-wedding-pink-500/30 w-full md:w-auto"
441
+ onPress={() => setShowRSVP(true)}
346
442
  >
347
443
  RSVP / Confirm Attendance
348
444
  </Button>
349
445
  ) : (
350
446
  <motion.div
351
- initial={{ opacity: 0, y: 20 }}
352
447
  animate={{ opacity: 1, y: 0 }}
353
448
  className="space-y-6 md:space-y-8 text-left bg-default-50 dark:bg-zinc-900/50 p-5 md:p-10 rounded-[24px] md:rounded-[32px] border border-default-200 dark:border-default-800"
449
+ initial={{ opacity: 0, y: 20 }}
354
450
  >
355
451
  <div className="flex justify-between items-center">
356
- <h3 className={`${fontSans.className} text-xl md:text-2xl font-bold text-default-800 dark:text-white`}>RSVP Details</h3>
357
- <Button isIconOnly variant="light" size="sm" onPress={() => setShowRSVP(false)}>✕</Button>
452
+ <h3
453
+ className={`${fontSans.className} text-xl md:text-2xl font-bold text-default-800 dark:text-white`}
454
+ >
455
+ RSVP Details
456
+ </h3>
457
+ <Button
458
+ isIconOnly
459
+ size="sm"
460
+ variant="light"
461
+ onPress={() => setShowRSVP(false)}
462
+ >
463
+
464
+ </Button>
358
465
  </div>
359
466
 
360
467
  <div className="grid md:grid-cols-2 gap-6 md:gap-8">
361
468
  <RadioGroup
469
+ classNames={{
470
+ label:
471
+ "text-wedding-pink-600 font-bold mb-1 text-sm md:text-base",
472
+ }}
362
473
  label="Will you attend?"
363
474
  value={rsvpData.attending}
364
- onValueChange={(val) => setRsvpData({ ...rsvpData, attending: val })}
365
- classNames={{ label: "text-wedding-pink-600 font-bold mb-1 text-sm md:text-base" }}
475
+ onValueChange={(val) =>
476
+ setRsvpData({ ...rsvpData, attending: val })
477
+ }
366
478
  >
367
- <Radio value="yes" classNames={{ label: "text-sm" }}>Yes, I'll be there!</Radio>
368
- <Radio value="no" classNames={{ label: "text-sm" }}>Sorry, I can't make it</Radio>
479
+ <Radio classNames={{ label: "text-sm" }} value="yes">
480
+ Yes, I&apos;ll be there!
481
+ </Radio>
482
+ <Radio classNames={{ label: "text-sm" }} value="no">
483
+ Sorry, I can&apos;t make it
484
+ </Radio>
369
485
  </RadioGroup>
370
486
 
371
487
  {rsvpData.attending === "yes" && (
372
488
  <RadioGroup
489
+ classNames={{
490
+ label:
491
+ "text-wedding-pink-600 font-bold mb-1 text-sm md:text-base",
492
+ }}
373
493
  label="Food Preference"
374
494
  value={rsvpData.food}
375
- onValueChange={(val) => setRsvpData({ ...rsvpData, food: val })}
376
- classNames={{ label: "text-wedding-pink-600 font-bold mb-1 text-sm md:text-base" }}
495
+ onValueChange={(val) =>
496
+ setRsvpData({ ...rsvpData, food: val })
497
+ }
377
498
  >
378
- <Radio value="non-veg" classNames={{ label: "text-sm" }}>Non-Vegetarian</Radio>
379
- <Radio value="veg" classNames={{ label: "text-sm" }}>Vegetarian</Radio>
499
+ <Radio
500
+ classNames={{ label: "text-sm" }}
501
+ value="non-veg"
502
+ >
503
+ Non-Vegetarian
504
+ </Radio>
505
+ <Radio classNames={{ label: "text-sm" }} value="veg">
506
+ Vegetarian
507
+ </Radio>
380
508
  </RadioGroup>
381
509
  )}
382
510
  </div>
383
511
 
384
512
  <div className="space-y-5 md:space-y-6">
385
- <Input
386
- label="Number of Guests"
387
- type="number"
388
- variant="bordered"
513
+ <Input
514
+ classNames={{
515
+ label:
516
+ "text-wedding-pink-600 font-bold text-xs md:text-sm",
517
+ inputWrapper: "border-wedding-pink-100",
518
+ input: "outline-none text-sm",
519
+ }}
520
+ label="Number of Guests"
389
521
  labelPlacement="outside-top"
522
+ type="number"
390
523
  value={String(rsvpData.guests)}
391
- onChange={(e) => setRsvpData({ ...rsvpData, guests: Number(e.target.value) })}
392
- classNames={{ label: "text-wedding-pink-600 font-bold text-xs md:text-sm", inputWrapper: "border-wedding-pink-100", input: "outline-none text-sm" }}
393
- />
394
- <Input
395
- label="Special Note / Allergies"
396
524
  variant="bordered"
525
+ onChange={(e) =>
526
+ setRsvpData({
527
+ ...rsvpData,
528
+ guests: Number(e.target.value),
529
+ })
530
+ }
531
+ />
532
+ <Input
533
+ classNames={{
534
+ label:
535
+ "text-wedding-pink-600 font-bold text-xs md:text-sm",
536
+ inputWrapper: "border-wedding-pink-100",
537
+ input: "outline-none text-sm",
538
+ }}
539
+ label="Special Note / Allergies"
397
540
  labelPlacement="outside-top"
398
541
  value={rsvpData.note}
399
- onChange={(e) => setRsvpData({ ...rsvpData, note: e.target.value })}
400
- classNames={{ label: "text-wedding-pink-600 font-bold text-xs md:text-sm", inputWrapper: "border-wedding-pink-100", input: "outline-none text-sm" }}
542
+ variant="bordered"
543
+ onChange={(e) =>
544
+ setRsvpData({ ...rsvpData, note: e.target.value })
545
+ }
401
546
  />
402
-
547
+
403
548
  <div className="p-4 bg-wedding-pink-50 dark:bg-wedding-pink-900/10 rounded-xl border border-wedding-pink-100 dark:border-wedding-pink-800/30">
404
- <p className="text-[9px] font-black text-wedding-pink-600 uppercase tracking-widest mb-1">Bot Protection</p>
405
- <Input
549
+ <p className="text-[9px] font-black text-wedding-pink-600 uppercase tracking-widest mb-1">
550
+ Bot Protection
551
+ </p>
552
+ <Input
406
553
  isRequired
407
- label="How many years have we been together?"
408
- placeholder="Answer in numbers"
409
- variant="underlined"
554
+ classNames={{
555
+ label: "text-[10px] md:text-xs",
556
+ input: "font-bold outline-none",
557
+ }}
558
+ label="How many years have we been together?"
410
559
  labelPlacement="outside-top"
560
+ placeholder="Answer in numbers"
411
561
  value={rsvpData.captcha}
412
- onChange={(e) => setRsvpData({ ...rsvpData, captcha: e.target.value })}
413
- classNames={{ label: "text-[10px] md:text-xs", input: "font-bold outline-none" }}
562
+ variant="underlined"
563
+ onChange={(e) =>
564
+ setRsvpData({
565
+ ...rsvpData,
566
+ captcha: e.target.value,
567
+ })
568
+ }
414
569
  />
415
570
  </div>
416
571
 
417
- <Button
572
+ <Button
573
+ className="w-full bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 text-white font-black h-12 md:h-14 text-base md:text-lg rounded-full"
418
574
  isLoading={rsvpLoading}
419
575
  onPress={handleRSVP}
420
- className="w-full bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 text-white font-black h-12 md:h-14 text-base md:text-lg rounded-full"
421
576
  >
422
577
  Confirm RSVP
423
578
  </Button>
@@ -427,6 +582,62 @@ export default function InvitationPage({
427
582
  </AnimatePresence>
428
583
  </div>
429
584
 
585
+ <Divider className="my-8 md:my-10 opacity-50" />
586
+
587
+ {/* Email Reminder Section */}
588
+ <div className="mb-10 px-0 md:px-10">
589
+ <div className="bg-default-50 dark:bg-zinc-900/50 p-6 rounded-2xl border border-default-200 dark:border-default-800">
590
+ <h3 className={`${fontSans.className} text-xl font-bold text-default-800 dark:text-white mb-4`}>
591
+ Get Event Reminders
592
+ </h3>
593
+ {hasEmailReminder && (
594
+ <div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-xl border border-blue-100 dark:border-blue-800/30 flex items-center justify-center gap-2">
595
+ <CheckIcon className="w-4 h-4 text-blue-500" />
596
+ <p className="text-xs font-medium text-blue-700 dark:text-blue-300">
597
+ Email reminder already set for your invitation.
598
+ </p>
599
+ </div>
600
+ )}
601
+ <p className="text-default-600 dark:text-gray-400 text-sm mb-6">
602
+ Leave your email to receive a reminder before the event starts.
603
+ </p>
604
+ <form
605
+ action={async (formData) => {
606
+ const { sendEmailReminder } = await import("../actions");
607
+
608
+ formData.append("slug", slug);
609
+ await sendEmailReminder(formData);
610
+ setHasEmailReminder(true);
611
+ addToast({
612
+ title: "Reminder Set",
613
+ description: "We will notify you before the event!",
614
+ color: "success",
615
+ });
616
+ }}
617
+ className="flex flex-col sm:flex-row gap-4 items-end"
618
+ >
619
+ <Input
620
+ isRequired
621
+ className="flex-1"
622
+ classNames={{
623
+ input: "outline-none",
624
+ inputWrapper: "bg-white dark:bg-zinc-900",
625
+ }}
626
+ name="email"
627
+ placeholder="john@example.com"
628
+ type="email"
629
+ aria-label="Email Address"
630
+ />
631
+ <Button
632
+ className="w-full sm:w-auto bg-wedding-pink-500 text-white font-semibold shadow-md"
633
+ type="submit"
634
+ >
635
+ Notify Me
636
+ </Button>
637
+ </form>
638
+ </div>
639
+ </div>
640
+
430
641
  <Divider className="my-8 md:my-10 opacity-50" />
431
642
  <div className="space-y-6">
432
643
  <p
@@ -443,6 +654,14 @@ export default function InvitationPage({
443
654
  >
444
655
  Meet the Couple
445
656
  </Button>
657
+ <Button
658
+ as={Link}
659
+ className="bg-wedding-pink-500 text-white shadow-lg hover:bg-wedding-pink-600 font-semibold w-full sm:w-auto"
660
+ href="/game"
661
+ size="lg"
662
+ >
663
+ Play Couple Quiz
664
+ </Button>
446
665
  <Button
447
666
  as={Link}
448
667
  className="border-wedding-pink-500 text-wedding-pink-500 hover:bg-wedding-pink-50 dark:hover:bg-wedding-pink-900/20 font-semibold w-full sm:w-auto"