@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.
- package/.eslintignore +20 -0
- package/.eslintrc.json +93 -0
- package/.recover +9 -0
- package/.vscode/settings.json +3 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/app/Neo-Lucentism/layout.tsx +7 -0
- package/app/Neo-Lucentism/page.tsx +259 -0
- package/app/couple/layout.tsx +7 -0
- package/app/couple/page.tsx +164 -0
- package/app/error.tsx +31 -0
- package/app/guestbook/page.tsx +470 -0
- package/app/invitation/[slug]/layout.tsx +36 -0
- package/app/invitation/[slug]/page.tsx +462 -0
- package/app/invitation/maker/auth.js +165 -0
- package/app/invitation/maker/dashboard.js +81 -0
- package/app/invitation/maker/guestAdder.js +204 -0
- package/app/invitation/maker/guestShower.js +287 -0
- package/app/invitation/maker/layout.tsx +11 -0
- package/app/invitation/maker/page.js +168 -0
- package/app/invitation/maker/rsvpViewer.js +122 -0
- package/app/layout.tsx +98 -0
- package/app/mark-the-dates/layout.tsx +7 -0
- package/app/mark-the-dates/page.tsx +196 -0
- package/app/memories/layout.tsx +7 -0
- package/app/memories/page.tsx +29 -0
- package/app/page.tsx +5 -0
- package/app/providers.tsx +33 -0
- package/app/sagun/layout.tsx +7 -0
- package/app/sagun/page.tsx +348 -0
- package/app/song-requests/page.tsx +354 -0
- package/app/sukanya/layout.tsx +7 -0
- package/app/sukanya/page.tsx +167 -0
- package/app/titas/layout.tsx +7 -0
- package/app/titas/page.tsx +175 -0
- package/app/travel-guide/page.tsx +400 -0
- package/app/updates/maker/page.js +323 -0
- package/app/updates/overlay/page.tsx +144 -0
- package/app/updates/page.js +207 -0
- package/cli.mjs +196 -0
- package/components/ConciergeBot.tsx +203 -0
- package/components/CountdownTimer.tsx +137 -0
- package/components/Gallery.tsx +372 -0
- package/components/LiveVideos.tsx +173 -0
- package/components/OurStory.tsx +160 -0
- package/components/certificate.jsx +300 -0
- package/components/counter.tsx +14 -0
- package/components/footer.tsx +89 -0
- package/components/hero.tsx +136 -0
- package/components/icons.tsx +283 -0
- package/components/importantNews.js +168 -0
- package/components/navbar.tsx +106 -0
- package/components/primitives.ts +53 -0
- package/components/sagun.js +22 -0
- package/components/theme-switch.tsx +81 -0
- package/components/updates.tsx +118 -0
- package/components/weddingcard.js +68 -0
- package/components/weddingcard2.js +58 -0
- package/config/firebase-admin.js +17 -0
- package/config/firebase.ts +36 -0
- package/config/fonts.ts +21 -0
- package/config/site.ts +74 -0
- package/next-env.d.ts +6 -0
- package/next.config.js +4 -0
- package/package.json +64 -0
- package/postcss.config.js +6 -0
- package/public/DCV.gif +0 -0
- package/public/DCV2.gif +0 -0
- package/public/DCV3.gif +0 -0
- package/public/Images/1.jpg +0 -0
- package/public/Images/11.jpg +0 -0
- package/public/Images/12.jpg +0 -0
- package/public/Images/13.jpg +0 -0
- package/public/Images/14.jpg +0 -0
- package/public/Images/15.jpg +0 -0
- package/public/Images/16.jpg +0 -0
- package/public/Images/17.jpg +0 -0
- package/public/Images/18.jpg +0 -0
- package/public/Images/19.jpg +0 -0
- package/public/Images/2.jpg +0 -0
- package/public/Images/20.jpg +0 -0
- package/public/Images/21.jpg +0 -0
- package/public/Images/22.jpg +0 -0
- package/public/Images/3.jpg +0 -0
- package/public/Images/4.jpg +0 -0
- package/public/Images/5.jpg +0 -0
- package/public/Images/6.jpg +0 -0
- package/public/Images/7.jpg +0 -0
- package/public/Images/8.jpg +0 -0
- package/public/Images/9.jpg +0 -0
- package/public/Images/9b.jpg +0 -0
- package/public/Images/Patipatra.jpeg +0 -0
- package/public/audio (1).mp3 +0 -0
- package/public/audio (2).mp3 +0 -0
- package/public/bride.jpg +0 -0
- package/public/corner1-01.svg +1 -0
- package/public/favicon.ico +0 -0
- package/public/groom.jpg +0 -0
- package/public/invite.png +0 -0
- package/public/love-birds.png +0 -0
- package/public/next.svg +1 -0
- package/public/pubqr.png +0 -0
- package/public/pw/001.jpg +0 -0
- package/public/pw/002.jpg +0 -0
- package/public/pw/003.jpg +0 -0
- package/public/pw/004.jpg +0 -0
- package/public/pw/005.jpg +0 -0
- package/public/pw/006.jpg +0 -0
- package/public/pw/007.jpg +0 -0
- package/public/pw/008.jpg +0 -0
- package/public/pw/009.jpg +0 -0
- package/public/pw/010.jpg +0 -0
- package/public/pw/011.jpg +0 -0
- package/public/pw/012.jpg +0 -0
- package/public/pw/013.jpg +0 -0
- package/public/pw/014.jpg +0 -0
- package/public/pw/015.jpg +0 -0
- package/public/pw/016.jpg +0 -0
- package/public/pw/017.jpg +0 -0
- package/public/pw/018.jpg +0 -0
- package/public/pw/019.jpg +0 -0
- package/public/pw/020.jpg +0 -0
- package/public/pw/021.jpg +0 -0
- package/public/pw/022.jpg +0 -0
- package/public/pw/023.jpg +0 -0
- package/public/pw/024.jpg +0 -0
- package/public/pw/025.jpg +0 -0
- package/public/pw/026.jpg +0 -0
- package/public/pw/027.jpg +0 -0
- package/public/pw/028.jpg +0 -0
- package/public/pw/029.jpg +0 -0
- package/public/pw/030.jpg +0 -0
- package/public/pw/031.jpg +0 -0
- package/public/pw/032.jpg +0 -0
- package/public/qr.png +0 -0
- package/public/vercel.svg +1 -0
- package/styles/globals.css +3 -0
- package/tailwind.config.js +51 -0
- package/tsconfig.json +45 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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'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;
|