@titas_mallick/wedding-site-gen 1.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 (141) hide show
  1. package/.eslintignore +20 -0
  2. package/.eslintrc.json +93 -0
  3. package/.recover +9 -0
  4. package/.vscode/settings.json +3 -0
  5. package/LICENSE +21 -0
  6. package/README.md +83 -0
  7. package/app/Neo-Lucentism/layout.tsx +7 -0
  8. package/app/Neo-Lucentism/page.tsx +259 -0
  9. package/app/couple/layout.tsx +7 -0
  10. package/app/couple/page.tsx +164 -0
  11. package/app/error.tsx +31 -0
  12. package/app/guestbook/page.tsx +470 -0
  13. package/app/invitation/[slug]/layout.tsx +36 -0
  14. package/app/invitation/[slug]/page.tsx +462 -0
  15. package/app/invitation/maker/auth.js +165 -0
  16. package/app/invitation/maker/dashboard.js +81 -0
  17. package/app/invitation/maker/guestAdder.js +204 -0
  18. package/app/invitation/maker/guestShower.js +287 -0
  19. package/app/invitation/maker/layout.tsx +11 -0
  20. package/app/invitation/maker/page.js +168 -0
  21. package/app/invitation/maker/rsvpViewer.js +122 -0
  22. package/app/layout.tsx +98 -0
  23. package/app/mark-the-dates/layout.tsx +7 -0
  24. package/app/mark-the-dates/page.tsx +196 -0
  25. package/app/memories/layout.tsx +7 -0
  26. package/app/memories/page.tsx +29 -0
  27. package/app/page.tsx +5 -0
  28. package/app/providers.tsx +33 -0
  29. package/app/sagun/layout.tsx +7 -0
  30. package/app/sagun/page.tsx +348 -0
  31. package/app/song-requests/page.tsx +354 -0
  32. package/app/sukanya/layout.tsx +7 -0
  33. package/app/sukanya/page.tsx +167 -0
  34. package/app/titas/layout.tsx +7 -0
  35. package/app/titas/page.tsx +175 -0
  36. package/app/travel-guide/page.tsx +400 -0
  37. package/app/updates/maker/page.js +323 -0
  38. package/app/updates/overlay/page.tsx +144 -0
  39. package/app/updates/page.js +207 -0
  40. package/cli.mjs +196 -0
  41. package/components/ConciergeBot.tsx +203 -0
  42. package/components/CountdownTimer.tsx +137 -0
  43. package/components/Gallery.tsx +372 -0
  44. package/components/LiveVideos.tsx +173 -0
  45. package/components/OurStory.tsx +160 -0
  46. package/components/certificate.jsx +300 -0
  47. package/components/counter.tsx +14 -0
  48. package/components/footer.tsx +89 -0
  49. package/components/hero.tsx +136 -0
  50. package/components/icons.tsx +283 -0
  51. package/components/importantNews.js +168 -0
  52. package/components/navbar.tsx +106 -0
  53. package/components/primitives.ts +53 -0
  54. package/components/sagun.js +22 -0
  55. package/components/theme-switch.tsx +81 -0
  56. package/components/updates.tsx +118 -0
  57. package/components/weddingcard.js +68 -0
  58. package/components/weddingcard2.js +58 -0
  59. package/config/firebase-admin.js +17 -0
  60. package/config/firebase.ts +36 -0
  61. package/config/fonts.ts +21 -0
  62. package/config/site.ts +74 -0
  63. package/next-env.d.ts +6 -0
  64. package/next.config.js +4 -0
  65. package/package.json +64 -0
  66. package/postcss.config.js +6 -0
  67. package/public/DCV.gif +0 -0
  68. package/public/DCV2.gif +0 -0
  69. package/public/DCV3.gif +0 -0
  70. package/public/Images/1.jpg +0 -0
  71. package/public/Images/11.jpg +0 -0
  72. package/public/Images/12.jpg +0 -0
  73. package/public/Images/13.jpg +0 -0
  74. package/public/Images/14.jpg +0 -0
  75. package/public/Images/15.jpg +0 -0
  76. package/public/Images/16.jpg +0 -0
  77. package/public/Images/17.jpg +0 -0
  78. package/public/Images/18.jpg +0 -0
  79. package/public/Images/19.jpg +0 -0
  80. package/public/Images/2.jpg +0 -0
  81. package/public/Images/20.jpg +0 -0
  82. package/public/Images/21.jpg +0 -0
  83. package/public/Images/22.jpg +0 -0
  84. package/public/Images/3.jpg +0 -0
  85. package/public/Images/4.jpg +0 -0
  86. package/public/Images/5.jpg +0 -0
  87. package/public/Images/6.jpg +0 -0
  88. package/public/Images/7.jpg +0 -0
  89. package/public/Images/8.jpg +0 -0
  90. package/public/Images/9.jpg +0 -0
  91. package/public/Images/9b.jpg +0 -0
  92. package/public/Images/Patipatra.jpeg +0 -0
  93. package/public/audio (1).mp3 +0 -0
  94. package/public/audio (2).mp3 +0 -0
  95. package/public/bride.jpg +0 -0
  96. package/public/corner1-01.svg +1 -0
  97. package/public/favicon.ico +0 -0
  98. package/public/groom.jpg +0 -0
  99. package/public/invite.png +0 -0
  100. package/public/love-birds.png +0 -0
  101. package/public/next.svg +1 -0
  102. package/public/pubqr.png +0 -0
  103. package/public/pw/001.jpg +0 -0
  104. package/public/pw/002.jpg +0 -0
  105. package/public/pw/003.jpg +0 -0
  106. package/public/pw/004.jpg +0 -0
  107. package/public/pw/005.jpg +0 -0
  108. package/public/pw/006.jpg +0 -0
  109. package/public/pw/007.jpg +0 -0
  110. package/public/pw/008.jpg +0 -0
  111. package/public/pw/009.jpg +0 -0
  112. package/public/pw/010.jpg +0 -0
  113. package/public/pw/011.jpg +0 -0
  114. package/public/pw/012.jpg +0 -0
  115. package/public/pw/013.jpg +0 -0
  116. package/public/pw/014.jpg +0 -0
  117. package/public/pw/015.jpg +0 -0
  118. package/public/pw/016.jpg +0 -0
  119. package/public/pw/017.jpg +0 -0
  120. package/public/pw/018.jpg +0 -0
  121. package/public/pw/019.jpg +0 -0
  122. package/public/pw/020.jpg +0 -0
  123. package/public/pw/021.jpg +0 -0
  124. package/public/pw/022.jpg +0 -0
  125. package/public/pw/023.jpg +0 -0
  126. package/public/pw/024.jpg +0 -0
  127. package/public/pw/025.jpg +0 -0
  128. package/public/pw/026.jpg +0 -0
  129. package/public/pw/027.jpg +0 -0
  130. package/public/pw/028.jpg +0 -0
  131. package/public/pw/029.jpg +0 -0
  132. package/public/pw/030.jpg +0 -0
  133. package/public/pw/031.jpg +0 -0
  134. package/public/pw/032.jpg +0 -0
  135. package/public/qr.png +0 -0
  136. package/public/vercel.svg +1 -0
  137. package/styles/globals.css +3 -0
  138. package/tailwind.config.js +51 -0
  139. package/tsconfig.json +45 -0
  140. package/tsconfig.tsbuildinfo +1 -0
  141. package/types/index.ts +5 -0
@@ -0,0 +1,462 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, use } from "react";
4
+ import { getFirestore, doc, getDoc, addDoc, collection, serverTimestamp } from "firebase/firestore";
5
+ import {
6
+ Card,
7
+ CardBody,
8
+ Divider,
9
+ Tabs,
10
+ Tab,
11
+ Button,
12
+ Spinner,
13
+ Accordion,
14
+ AccordionItem,
15
+ Input,
16
+ RadioGroup,
17
+ Radio,
18
+ addToast,
19
+ } from "@heroui/react";
20
+ import { Link } from "@heroui/link";
21
+ import { motion, AnimatePresence } from "framer-motion";
22
+
23
+ import { fontCursive, fontSans, fontMono } from "@/config/fonts";
24
+ import firebaseApp from "@/config/firebase";
25
+ import { MapPinIcon, CalendarIcon, ClockIcon } from "@/components/icons";
26
+
27
+ export default function InvitationPage({
28
+ params,
29
+ }: {
30
+ params: Promise<{ slug: string }>;
31
+ }) {
32
+ const { slug } = use(params);
33
+
34
+ const [guest, setGuest] = useState<any>(null);
35
+ const [loading, setLoading] = useState(true);
36
+ const [notFound, setNotFound] = useState(false);
37
+ const [isTranslating, setIsTranslating] = useState(false);
38
+ const [translatedData, setTranslatedData] = useState<any>(null);
39
+
40
+ // RSVP States
41
+ const [showRSVP, setShowRSVP] = useState(false);
42
+ const [rsvpData, setRsvpData] = useState({ attending: "yes", guests: 1, food: "non-veg", note: "", captcha: "" });
43
+ const [rsvpLoading, setRsvpLoading] = useState(false);
44
+ const [hasRSVPed, setHasRSVPed] = useState(false);
45
+
46
+ const handleRSVP = async () => {
47
+ if (rsvpData.captcha.trim() !== "10") {
48
+ addToast({ title: "Security Check", description: "Please answer correctly (Hint: 10).", color: "warning" });
49
+ return;
50
+ }
51
+
52
+ setRsvpLoading(true);
53
+ try {
54
+ const db = getFirestore(firebaseApp());
55
+ await addDoc(collection(db, "rsvps"), {
56
+ guestId: slug,
57
+ guestName: guest.name,
58
+ ...rsvpData,
59
+ timestamp: serverTimestamp(),
60
+ });
61
+ setHasRSVPed(true);
62
+ addToast({ title: "RSVP Received", description: "Thank you for confirming!", color: "success" });
63
+ } catch (err) {
64
+ console.error("RSVP error", err);
65
+ addToast({ title: "Error", description: "Failed to send RSVP. Please try again.", color: "danger" });
66
+ } finally {
67
+ setRsvpLoading(false);
68
+ }
69
+ };
70
+
71
+ const translateContent = async () => {
72
+ // ... rest of translation logic remains same
73
+ };
74
+
75
+ useEffect(() => {
76
+ const fetchData = async () => {
77
+ try {
78
+ const db = getFirestore(firebaseApp());
79
+ const docRef = doc(db, "invitation", slug);
80
+ const docSnap = await getDoc(docRef);
81
+
82
+ if (docSnap.exists()) {
83
+ const guestData = docSnap.data();
84
+ setGuest(guestData);
85
+ } else {
86
+ setNotFound(true);
87
+ }
88
+ } catch (error) {
89
+ setNotFound(true);
90
+ } finally {
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ if (slug) fetchData();
96
+ }, [slug]);
97
+
98
+ const events: any = {
99
+ registration: {
100
+ title: "Engagement Ceremony",
101
+ date: "23rd November 2025",
102
+ venue: "Srerampore",
103
+ 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
+ mapHref: "https://maps.app.goo.gl/diSeycey1hyqqtAu8",
105
+ time: "10:00 AM",
106
+ direction: "Near Battala",
107
+ },
108
+ wedding: {
109
+ title: "Wedding Ceremony",
110
+ date: "23rd January 2026",
111
+ venue: "Serampore",
112
+ 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
+ mapHref: "https://maps.app.goo.gl/G5R3bkcTwwa2d54R8",
114
+ time: "06:00 PM",
115
+ direction: "Near Belting Bazar",
116
+ },
117
+ reception: {
118
+ title: "Reception Celebration",
119
+ date: "25th January 2026",
120
+ venue: "Konnagar",
121
+ 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
+ mapHref: "https://maps.app.goo.gl/ubdTsy6tnYMsvSSXA",
123
+ time: "06:00 PM",
124
+ direction: "Near GT Road",
125
+ },
126
+ };
127
+
128
+ const renderEventDetails = (event: any) => (
129
+ <div className="pt-4 md:pt-8 grid md:grid-cols-2 gap-6 md:gap-8 text-left animate-in fade-in slide-in-from-bottom-4 duration-500">
130
+ {/* Details */}
131
+ <div className="space-y-6 bg-default-50 dark:bg-default-100/10 p-6 rounded-2xl border border-default-100 dark:border-default-700">
132
+ <div className="flex items-start gap-4">
133
+ <CalendarIcon className="w-6 h-6 text-wedding-pink-500 mt-1" />
134
+ <div>
135
+ <p className="font-bold text-default-800 dark:text-white">Date</p>
136
+ <p className="text-default-600 dark:text-gray-300">{event.date}</p>
137
+ </div>
138
+ </div>
139
+
140
+ <div className="flex items-start gap-4">
141
+ <ClockIcon className="w-6 h-6 text-wedding-gold-500 mt-1" />
142
+ <div>
143
+ <p className="font-bold text-default-800 dark:text-white">Time</p>
144
+ <p className="text-default-600 dark:text-gray-300">{event.time}</p>
145
+ </div>
146
+ </div>
147
+
148
+ <div className="flex items-start gap-4">
149
+ <MapPinIcon className="w-6 h-6 text-indigo-500 mt-1" />
150
+ <div>
151
+ <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>
153
+ <p className="text-default-500 text-sm mt-1">{event.direction}</p>
154
+ </div>
155
+ </div>
156
+
157
+ <Button
158
+ as={Link}
159
+ href={event.mapHref}
160
+ isExternal
161
+ 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"
162
+ startContent={<MapPinIcon className="w-4 h-4" />}
163
+ >
164
+ Open in Google Maps
165
+ </Button>
166
+ </div>
167
+
168
+ {/* Map Embed */}
169
+ <div className="h-64 md:h-auto rounded-2xl overflow-hidden shadow-lg border border-default-200 dark:border-default-700">
170
+ <iframe
171
+ title={`${event.title} Map`}
172
+ className="w-full h-full"
173
+ src={event.map}
174
+ loading="lazy"
175
+ allowFullScreen
176
+ ></iframe>
177
+ </div>
178
+ </div>
179
+ );
180
+
181
+ if (loading) {
182
+ return (
183
+ <div className="flex h-screen items-center justify-center">
184
+ <Spinner color="danger" size="lg" />
185
+ </div>
186
+ );
187
+ }
188
+
189
+ if (notFound) {
190
+ return (
191
+ <div className="flex h-screen flex-col items-center justify-center gap-4 text-center">
192
+ <h1 className={`${fontCursive.className} text-4xl text-default-800`}>
193
+ Invitation Not Found
194
+ </h1>
195
+ <p className="text-default-600">
196
+ We couldn&apos;t find an invitation for this link.
197
+ </p>
198
+ <Button as={Link} color="primary" href="/" variant="flat">
199
+ Return Home
200
+ </Button>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ return (
206
+ <div className="min-h-screen w-full py-10 px-4 flex items-center justify-center">
207
+ <motion.div
208
+ animate={{ opacity: 1, scale: 1 }}
209
+ className="w-full max-w-3xl"
210
+ initial={{ opacity: 0, scale: 0.95 }}
211
+ transition={{ duration: 0.8 }}
212
+ >
213
+ <Card className="w-full bg-white/90 dark:bg-black/80 backdrop-blur-xl border border-wedding-gold-200 dark:border-wedding-gold-800 shadow-2xl overflow-visible">
214
+ {/* Header Section */}
215
+ <div className="relative h-40 md:h-64 bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 rounded-t-xl overflow-hidden">
216
+ <div className="absolute inset-0 bg-[url('/corner1-01.svg')] opacity-20 bg-cover bg-center mix-blend-overlay" />
217
+ <div className="absolute inset-0 flex flex-col items-center justify-center text-white p-4 text-center">
218
+ <p
219
+ className={`${fontCursive.className} text-4xl md:text-7xl font-bold drop-shadow-md px-2`}
220
+ >
221
+ Titas & Sukanya
222
+ </p>
223
+ <p
224
+ className={`${fontMono.className} mt-2 md:mt-4 text-[10px] md:text-base uppercase tracking-widest opacity-90 px-4`}
225
+ >
226
+ Request the honor of your presence
227
+ </p>
228
+ </div>
229
+ {/* Translation Button */}
230
+ <Button
231
+ size="sm"
232
+ variant="flat"
233
+ 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
+ >
237
+ {translatedData ? "See English" : "বাংলায় দেখুন"}
238
+ </Button>
239
+ </div>
240
+
241
+ <CardBody className="px-4 py-8 md:px-12 md:py-14 text-center">
242
+ {/* Guest Personalization */}
243
+ <div className="mb-8 md:mb-10">
244
+ <p
245
+ className={`${fontCursive.className} text-3xl md:text-5xl text-wedding-pink-600 dark:text-wedding-pink-400 mb-4 md:mb-6`}
246
+ >
247
+ {translatedData ? "প্রিয়" : "Dear"} {translatedData?.guestName || guest.name}
248
+ {guest.invitedGuests > 1 && (
249
+ <span className="text-xl md:text-3xl ml-2">{translatedData ? "& পরিবার" : "& Family"}</span>
250
+ )}
251
+ ,
252
+ </p>
253
+
254
+ <div
255
+ className={`${fontSans.className} text-base md:text-xl text-default-700 dark:text-gray-200 leading-relaxed space-y-4 md:space-y-6 max-w-2xl mx-auto`}
256
+ >
257
+ <p>
258
+ {translatedData ? (
259
+ <>
260
+ {translatedData.welcomeNote || `${translatedData.familySide === "bride" ? "কনে" : "বর"}-র পক্ষ থেকে একজন প্রিয় ${translatedData.relation} হিসেবে আপনার উপস্থিতি আমাদের কাছে অনেক গুরুত্বপূর্ণ।`}
261
+ </>
262
+ ) : (
263
+ <>
264
+ As a cherished {guest.relation.toLowerCase()} from the{" "}
265
+ <span className="font-semibold text-wedding-gold-600 dark:text-wedding-gold-400">
266
+ {guest.familySide === "bride" ? "Bride's" : "Groom's"}
267
+ </span>{" "}
268
+ side, your presence would mean the world to us as we begin
269
+ this beautiful new chapter.
270
+ </>
271
+ )}
272
+ </p>
273
+ <p>
274
+ {translatedData ? "আমরা আপনাকে নিম্নলিখিত অনুষ্ঠানে যোগ দেওয়ার জন্য সাদর আমন্ত্রণ জানাচ্ছি:" : `We warmly invite you to join us for the following celebration${guest.invitedFor.length > 1 ? "s" : ""}:`}
275
+ </p>
276
+ </div>
277
+ </div>
278
+
279
+ {/* Event Tabs / Accordion */}
280
+ <div className="w-full">
281
+ {/* Desktop Tabs */}
282
+ <div className="hidden md:block">
283
+ <Tabs
284
+ aria-label="Wedding Events"
285
+ color="danger"
286
+ variant="underlined"
287
+ classNames={{
288
+ tabList: "gap-6 w-full relative rounded-none p-0 border-b border-divider",
289
+ cursor: "w-full bg-wedding-pink-500",
290
+ tab: "max-w-fit px-0 h-12",
291
+ tabContent: "group-data-[selected=true]:text-wedding-pink-500 font-medium text-lg"
292
+ }}
293
+ >
294
+ {guest.invitedFor.map((eventKey: string) => (
295
+ <Tab key={eventKey} title={events[eventKey].title}>
296
+ {renderEventDetails(events[eventKey])}
297
+ </Tab>
298
+ ))}
299
+ </Tabs>
300
+ </div>
301
+
302
+ {/* Mobile Accordion */}
303
+ <div className="md:hidden text-left">
304
+ <Accordion
305
+ variant="splitted"
306
+ selectionMode="multiple"
307
+ defaultExpandedKeys={guest.invitedFor}
308
+ className="px-0"
309
+ >
310
+ {guest.invitedFor.map((eventKey: string) => (
311
+ <AccordionItem
312
+ key={eventKey}
313
+ aria-label={events[eventKey].title}
314
+ title={
315
+ <span className="font-bold text-wedding-pink-600 dark:text-wedding-pink-400">
316
+ {events[eventKey].title}
317
+ </span>
318
+ }
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
+ >
321
+ {renderEventDetails(events[eventKey])}
322
+ </AccordionItem>
323
+ ))}
324
+ </Accordion>
325
+ </div>
326
+ </div>
327
+
328
+ <Divider className="my-10 opacity-50" />
329
+
330
+ {/* RSVP Section */}
331
+ <div className="mb-10 px-0 md:px-10">
332
+ <AnimatePresence mode="wait">
333
+ {hasRSVPed ? (
334
+ <motion.div
335
+ initial={{ opacity: 0, scale: 0.9 }}
336
+ animate={{ opacity: 1, scale: 1 }}
337
+ 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"
338
+ >
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>
341
+ </motion.div>
342
+ ) : !showRSVP ? (
343
+ <Button
344
+ onPress={() => setShowRSVP(true)}
345
+ 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"
346
+ >
347
+ RSVP / Confirm Attendance
348
+ </Button>
349
+ ) : (
350
+ <motion.div
351
+ initial={{ opacity: 0, y: 20 }}
352
+ animate={{ opacity: 1, y: 0 }}
353
+ 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"
354
+ >
355
+ <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>
358
+ </div>
359
+
360
+ <div className="grid md:grid-cols-2 gap-6 md:gap-8">
361
+ <RadioGroup
362
+ label="Will you attend?"
363
+ 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" }}
366
+ >
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>
369
+ </RadioGroup>
370
+
371
+ {rsvpData.attending === "yes" && (
372
+ <RadioGroup
373
+ label="Food Preference"
374
+ 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" }}
377
+ >
378
+ <Radio value="non-veg" classNames={{ label: "text-sm" }}>Non-Vegetarian</Radio>
379
+ <Radio value="veg" classNames={{ label: "text-sm" }}>Vegetarian</Radio>
380
+ </RadioGroup>
381
+ )}
382
+ </div>
383
+
384
+ <div className="space-y-5 md:space-y-6">
385
+ <Input
386
+ label="Number of Guests"
387
+ type="number"
388
+ variant="bordered"
389
+ labelPlacement="outside-top"
390
+ 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
+ variant="bordered"
397
+ labelPlacement="outside-top"
398
+ 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" }}
401
+ />
402
+
403
+ <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
406
+ isRequired
407
+ label="How many years have we been together?"
408
+ placeholder="Answer in numbers"
409
+ variant="underlined"
410
+ labelPlacement="outside-top"
411
+ 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" }}
414
+ />
415
+ </div>
416
+
417
+ <Button
418
+ isLoading={rsvpLoading}
419
+ 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
+ >
422
+ Confirm RSVP
423
+ </Button>
424
+ </div>
425
+ </motion.div>
426
+ )}
427
+ </AnimatePresence>
428
+ </div>
429
+
430
+ <Divider className="my-8 md:my-10 opacity-50" />
431
+ <div className="space-y-6">
432
+ <p
433
+ className={`${fontCursive.className} text-3xl md:text-4xl text-default-800 dark:text-white`}
434
+ >
435
+ With Love & Anticipation,
436
+ </p>
437
+ <div className="flex flex-col sm:flex-row justify-center gap-3 md:gap-4">
438
+ <Button
439
+ as={Link}
440
+ className="bg-wedding-gold-500 text-white shadow-lg hover:bg-wedding-gold-600 font-semibold w-full sm:w-auto"
441
+ href="/couple"
442
+ size="lg"
443
+ >
444
+ Meet the Couple
445
+ </Button>
446
+ <Button
447
+ as={Link}
448
+ 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"
449
+ href="/sagun"
450
+ size="lg"
451
+ variant="bordered"
452
+ >
453
+ Send Wishes
454
+ </Button>
455
+ </div>
456
+ </div>
457
+ </CardBody>
458
+ </Card>
459
+ </motion.div>
460
+ </div>
461
+ );
462
+ }
@@ -0,0 +1,165 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ Button,
6
+ Modal,
7
+ ModalContent,
8
+ ModalHeader,
9
+ ModalBody,
10
+ ModalFooter,
11
+ Input,
12
+ addToast,
13
+ Chip,
14
+ } from "@heroui/react";
15
+ import firebaseApp from "@/config/firebase";
16
+ import {
17
+ getAuth,
18
+ signInWithEmailAndPassword,
19
+ onAuthStateChanged,
20
+ signOut,
21
+ } from "firebase/auth";
22
+
23
+ const Auth = ({ userSet }) => {
24
+ const [modalOpen, setModalOpen] = useState(false);
25
+ const [credentials, setCredentials] = useState({
26
+ username: "",
27
+ password: "",
28
+ });
29
+ const [user, setUser] = useState(null);
30
+
31
+ const auth = getAuth(firebaseApp());
32
+
33
+ useEffect(() => {
34
+ const unsubscribe = onAuthStateChanged(auth, (user) => {
35
+ setUser(user || null);
36
+ userSet(user || null);
37
+ });
38
+ return () => unsubscribe();
39
+ }, []);
40
+
41
+ const handleChange = (e) => {
42
+ const { name, value } = e.target;
43
+ setCredentials((prev) => ({ ...prev, [name]: value }));
44
+ };
45
+
46
+ const handleSubmit = async (e) => {
47
+ e.preventDefault();
48
+ try {
49
+ await signInWithEmailAndPassword(
50
+ auth,
51
+ credentials.username,
52
+ credentials.password
53
+ );
54
+ setCredentials({ username: "", password: "" });
55
+ setModalOpen(false);
56
+ addToast({
57
+ title: "Signed in",
58
+ description: "You have been signed in successfully.",
59
+ color: "success",
60
+ variant: "solid",
61
+ radius: "lg",
62
+ closeIcon: true,
63
+ timeout: 2000,
64
+ });
65
+ } catch (error) {
66
+ addToast({
67
+ title: "Sign-in failed",
68
+ description: "Invalid username or password.",
69
+ color: "danger",
70
+ variant: "solid",
71
+ radius: "lg",
72
+ closeIcon: true,
73
+ timeout: 2000,
74
+ });
75
+ }
76
+ };
77
+
78
+ const handleSignOut = async () => {
79
+ try {
80
+ await signOut(auth);
81
+ userSet(null);
82
+ addToast({
83
+ title: "Signed out",
84
+ description: "You have been signed out successfully.",
85
+ color: "danger",
86
+ variant: "solid",
87
+ radius: "lg",
88
+ closeIcon: true,
89
+ timeout: 2000,
90
+ });
91
+ } catch (error) {
92
+ console.error("❌ Sign-out error:", error.message);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <>
98
+ <section className="flex items-center justify-between p-4 rounded-lg">
99
+ {!user ? (
100
+ <Button color="primary" onPress={() => setModalOpen(true)}>
101
+ Sign In
102
+ </Button>
103
+ ) : (
104
+ <Button color="danger" onPress={handleSignOut}>
105
+ Sign Out
106
+ </Button>
107
+ )}
108
+ <Chip color={user ? "success" : "danger"}>
109
+ {user ? (
110
+ <>
111
+ Signed in as <strong className="ml-1">{user.email}</strong>
112
+ </>
113
+ ) : (
114
+ "Not signed in"
115
+ )}
116
+ </Chip>
117
+ </section>
118
+
119
+ <Modal isOpen={modalOpen} onOpenChange={setModalOpen} backdrop="blur">
120
+ <ModalContent>
121
+ <form onSubmit={handleSubmit}>
122
+ <ModalHeader className="text-xl">Sign In</ModalHeader>
123
+ <ModalBody className="gap-4">
124
+ <Input
125
+ isRequired
126
+ name="username"
127
+ label="Username"
128
+ labelPlacement="outside-top"
129
+ placeholder="Enter your username"
130
+ value={credentials.username}
131
+ onChange={handleChange}
132
+ classNames={{
133
+ input: "outline-none",
134
+ }}
135
+ />
136
+ <Input
137
+ isRequired
138
+ type="password"
139
+ name="password"
140
+ label="Password"
141
+ labelPlacement="outside-top"
142
+ placeholder="Enter your password"
143
+ value={credentials.password}
144
+ onChange={handleChange}
145
+ classNames={{
146
+ input: "outline-none",
147
+ }}
148
+ />
149
+ </ModalBody>
150
+ <ModalFooter>
151
+ <Button variant="light" onPress={() => setModalOpen(false)}>
152
+ Cancel
153
+ </Button>
154
+ <Button type="submit" color="primary">
155
+ Submit
156
+ </Button>
157
+ </ModalFooter>
158
+ </form>
159
+ </ModalContent>
160
+ </Modal>
161
+ </>
162
+ );
163
+ };
164
+
165
+ export default Auth;