@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,323 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import {
5
+ Textarea,
6
+ Button,
7
+ Card,
8
+ CardHeader,
9
+ CardBody,
10
+ addToast,
11
+ Chip,
12
+ Divider,
13
+ } from "@heroui/react";
14
+ import {
15
+ getFirestore,
16
+ collection,
17
+ addDoc,
18
+ serverTimestamp,
19
+ } from "firebase/firestore";
20
+ import { motion, AnimatePresence } from "framer-motion";
21
+ import { getAuth, onAuthStateChanged, signOut } from "firebase/auth";
22
+
23
+ import firebaseApp from "@/config/firebase";
24
+ import { fontCursive, fontSans, fontMono } from "@/config/fonts";
25
+ import { HeartFilledIcon } from "@/components/icons";
26
+ import Auth from "../../invitation/maker/auth.js";
27
+
28
+ const auth = getAuth(firebaseApp());
29
+ const db = getFirestore(firebaseApp());
30
+
31
+ const UpdateMaker = () => {
32
+ const [user, setUser] = useState(null);
33
+ const [text, setText] = useState("");
34
+ const [loading, setLoading] = useState(false);
35
+ const [isRefining, setIsRefining] = useState(false);
36
+
37
+ useEffect(() => {
38
+ const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
39
+ setUser(currentUser);
40
+ });
41
+ return () => unsubscribe();
42
+ }, []);
43
+
44
+ const handleRefine = async () => {
45
+ if (!text.trim()) {
46
+ addToast({
47
+ title: "Nothing to refine",
48
+ description: "Please write something first.",
49
+ color: "warning",
50
+ });
51
+ return;
52
+ }
53
+
54
+ setIsRefining(true);
55
+ try {
56
+ const apiKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY;
57
+
58
+ if (!apiKey) {
59
+ throw new Error("Gemini API Key is missing. Please check your .env.local file and restart the server.");
60
+ }
61
+
62
+ const response = await fetch(
63
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`,
64
+ {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({
68
+ contents: [{
69
+ parts: [{
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.
72
+ Constraint 1: Output ONLY the refined message in PLAIN TEXT.
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.
75
+
76
+ Message to rewrite: "${text}"`
77
+ }]
78
+ }],
79
+ generationConfig: {
80
+ maxOutputTokens: 300,
81
+ temperature: 0.7,
82
+ }
83
+ })
84
+ }
85
+ );
86
+
87
+ const data = await response.json();
88
+
89
+ if (data.error) {
90
+ throw new Error(data.error.message || "API Error");
91
+ }
92
+
93
+ const refinedText = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
94
+
95
+ if (refinedText) {
96
+ setText(refinedText);
97
+ addToast({
98
+ title: "Refined ✨",
99
+ description: "Message polished successfully!",
100
+ color: "success",
101
+ variant: "solid",
102
+ });
103
+ } else {
104
+ throw new Error("AI returned an empty response. Try rephrasing your input.");
105
+ }
106
+ } catch (err) {
107
+ console.error("AI Refinement error:", err);
108
+ addToast({
109
+ title: "Refinement Failed",
110
+ description: err.message || "An unexpected error occurred.",
111
+ color: "danger",
112
+ });
113
+ } finally {
114
+ setIsRefining(false);
115
+ }
116
+ };
117
+
118
+ const handleSave = async () => {
119
+ if (text.trim().length === 0 || text.length > 300) {
120
+ addToast({
121
+ title: "Invalid input",
122
+ description: "Please enter 1–300 characters.",
123
+ color: "warning",
124
+ variant: "solid",
125
+ radius: "lg",
126
+ });
127
+ return;
128
+ }
129
+
130
+ setLoading(true);
131
+ try {
132
+ await addDoc(collection(db, "updates"), {
133
+ text: text.trim(),
134
+ createdAt: serverTimestamp(),
135
+ uid: user.uid,
136
+ email: user.email,
137
+ });
138
+ setText("");
139
+ addToast({
140
+ title: "Success",
141
+ description: "Your update has been shared with everyone!",
142
+ color: "success",
143
+ variant: "solid",
144
+ radius: "lg",
145
+ });
146
+ } catch (err) {
147
+ addToast({
148
+ title: "Error",
149
+ description: "Something went wrong. Please try again.",
150
+ color: "danger",
151
+ variant: "solid",
152
+ radius: "lg",
153
+ });
154
+ }
155
+ setLoading(false);
156
+ };
157
+
158
+ const handleSignOut = async () => {
159
+ try {
160
+ await signOut(auth);
161
+ setUser(null);
162
+ addToast({
163
+ title: "Signed Out",
164
+ description: "Session closed successfully.",
165
+ color: "default",
166
+ variant: "flat",
167
+ radius: "lg",
168
+ });
169
+ } catch (error) {
170
+ console.error("Sign out error", error);
171
+ }
172
+ };
173
+
174
+ return (
175
+ <div className="min-h-[80vh] flex flex-col items-center justify-center p-6 bg-gradient-to-b from-transparent to-wedding-pink-50/20">
176
+ <motion.div
177
+ initial={{ opacity: 0, scale: 0.95 }}
178
+ animate={{ opacity: 1, scale: 1 }}
179
+ className="w-full max-w-xl"
180
+ >
181
+ <Card className="w-full bg-white/80 dark:bg-black/60 backdrop-blur-xl border border-wedding-gold-200 dark:border-wedding-gold-800 shadow-2xl overflow-visible">
182
+ {/* Decorative Top Border */}
183
+ <div className="absolute top-0 left-0 w-full h-1.5 bg-gradient-to-r from-wedding-pink-500 via-wedding-gold-500 to-wedding-pink-500 rounded-t-xl" />
184
+
185
+ <CardHeader className="flex flex-col items-center pt-10 pb-4 text-center">
186
+ <motion.div
187
+ initial={{ rotate: -10, scale: 0 }}
188
+ animate={{ rotate: 0, scale: 1 }}
189
+ transition={{ type: "spring", damping: 10 }}
190
+ className="bg-wedding-pink-100 dark:bg-wedding-pink-900/30 p-4 rounded-full mb-4"
191
+ >
192
+ <HeartFilledIcon className="w-8 h-8 text-wedding-pink-500" />
193
+ </motion.div>
194
+ <h1 className={`${fontCursive.className} text-5xl md:text-6xl text-wedding-pink-600 dark:text-wedding-pink-400`}>
195
+ Post an Update
196
+ </h1>
197
+ <p className={`${fontMono.className} mt-2 text-xs uppercase tracking-widest text-default-500`}>
198
+ Share a moment with your guests
199
+ </p>
200
+ </CardHeader>
201
+
202
+ <Divider className="mx-8 w-auto opacity-50" />
203
+
204
+ <CardBody className="px-8 py-10">
205
+ <AnimatePresence mode="wait">
206
+ {user ? (
207
+ <motion.div
208
+ key="editor"
209
+ initial={{ opacity: 0, y: 20 }}
210
+ animate={{ opacity: 1, y: 0 }}
211
+ exit={{ opacity: 0, y: -20 }}
212
+ className="space-y-6"
213
+ >
214
+ <div className="flex items-center justify-between">
215
+ <div className="flex items-center gap-2">
216
+ <Chip
217
+ variant="flat"
218
+ color="warning"
219
+ className="bg-wedding-gold-50 dark:bg-wedding-gold-900/20 text-wedding-gold-700 dark:text-wedding-gold-300"
220
+ size="sm"
221
+ >
222
+ Admin: {user.email}
223
+ </Chip>
224
+ </div>
225
+ <Button
226
+ size="sm"
227
+ variant="light"
228
+ color="danger"
229
+ className="font-semibold"
230
+ onPress={handleSignOut}
231
+ >
232
+ Sign Out
233
+ </Button>
234
+ </div>
235
+
236
+ <div className="relative">
237
+ <Textarea
238
+ label="Your Message"
239
+ placeholder="What's happening?"
240
+ variant="bordered"
241
+ className="w-full"
242
+ classNames={{
243
+ input: "text-lg h-32 py-2 outline-none",
244
+ label: "text-wedding-pink-600 font-semibold",
245
+ inputWrapper: "border-wedding-gold-100 hover:border-wedding-gold-300 focus-within:!border-wedding-pink-500 transition-colors",
246
+ }}
247
+ maxLength={300}
248
+ value={text}
249
+ onChange={(e) => setText(e.target.value)}
250
+ />
251
+ <div className={`absolute bottom-2 right-3 text-xs font-mono ${text.length >= 280 ? 'text-danger' : 'text-default-400'}`}>
252
+ {text.length}/300
253
+ </div>
254
+ </div>
255
+
256
+ <div className="flex flex-col gap-3">
257
+ <Button
258
+ variant="flat"
259
+ color="secondary"
260
+ size="sm"
261
+ className="w-fit self-end font-semibold bg-wedding-pink-50 dark:bg-wedding-pink-900/20 text-wedding-pink-600 dark:text-wedding-pink-400"
262
+ onPress={handleRefine}
263
+ isLoading={isRefining}
264
+ startContent={!isRefining && <span>✨</span>}
265
+ >
266
+ Refine with AI
267
+ </Button>
268
+
269
+ <Button
270
+ isLoading={loading}
271
+ color="danger"
272
+ size="lg"
273
+ className="w-full bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 text-white font-bold shadow-lg hover:opacity-90 transition-opacity"
274
+ onPress={handleSave}
275
+ isDisabled={text.trim().length === 0 || isRefining}
276
+ >
277
+ Share Update
278
+ </Button>
279
+ </div>
280
+ </motion.div>
281
+ ) : (
282
+ <motion.div
283
+ key="login"
284
+ initial={{ opacity: 0, y: 20 }}
285
+ animate={{ opacity: 1, y: 0 }}
286
+ exit={{ opacity: 0, y: -20 }}
287
+ className="flex flex-col items-center gap-6"
288
+ >
289
+ <p className="text-default-600 dark:text-gray-400 text-center font-medium italic">
290
+ You need to be signed in to post updates to the timeline.
291
+ </p>
292
+ <div className="w-full border-2 border-dashed border-wedding-pink-100 dark:border-wedding-pink-900/30 rounded-2xl p-4 bg-default-50/50">
293
+ <Auth userSet={setUser} />
294
+ </div>
295
+ </motion.div>
296
+ )}
297
+ </AnimatePresence>
298
+ </CardBody>
299
+ </Card>
300
+ </motion.div>
301
+
302
+ {/* Helper links */}
303
+ <motion.div
304
+ initial={{ opacity: 0 }}
305
+ animate={{ opacity: 1 }}
306
+ transition={{ delay: 0.5 }}
307
+ className="mt-8 flex flex-wrap justify-center gap-4"
308
+ >
309
+ <Button
310
+ as="a"
311
+ href="/updates"
312
+ variant="light"
313
+ color="primary"
314
+ className="text-wedding-pink-600 hover:text-wedding-pink-700"
315
+ >
316
+ View Public Feed →
317
+ </Button>
318
+ </motion.div>
319
+ </div>
320
+ );
321
+ };
322
+
323
+ export default UpdateMaker;
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ getFirestore,
6
+ collection,
7
+ onSnapshot,
8
+ query,
9
+ orderBy,
10
+ limit,
11
+ } from "firebase/firestore";
12
+ import { motion, AnimatePresence } from "framer-motion";
13
+
14
+ import firebaseApp from "@/config/firebase";
15
+ import { fontCursive, fontSans, fontMono } from "@/config/fonts";
16
+ import { HeartFilledIcon } from "@/components/icons";
17
+
18
+ const db = getFirestore(firebaseApp());
19
+
20
+ export default function UpdateOverlay() {
21
+ const [latestUpdate, setLatestUpdate] = useState<any>(null);
22
+ const [time, setTime] = useState(new Date());
23
+ const [hasMounted, setHasMounted] = useState(false);
24
+
25
+ useEffect(() => {
26
+ setHasMounted(true);
27
+ const timer = setInterval(() => setTime(new Date()), 1000);
28
+ const q = query(
29
+ collection(db, "updates"),
30
+ orderBy("createdAt", "desc"),
31
+ limit(1)
32
+ );
33
+
34
+ const unsubscribe = onSnapshot(q, (snapshot) => {
35
+ if (!snapshot.empty) {
36
+ setLatestUpdate({
37
+ id: snapshot.docs[0].id,
38
+ ...snapshot.docs[0].data(),
39
+ });
40
+ }
41
+ });
42
+
43
+ return () => {
44
+ unsubscribe();
45
+ clearInterval(timer);
46
+ };
47
+ }, []);
48
+
49
+ return (
50
+ <div className="fixed inset-0 flex flex-col justify-between p-16 pointer-events-none">
51
+ {/* 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;
59
+ }
60
+ body {
61
+ background: #00ff00 !important;
62
+ overflow: hidden;
63
+ }
64
+ `}</style>
65
+
66
+ {/* Top Bar: Clock & Brand */}
67
+ <div className="flex justify-between items-start">
68
+ <motion.div
69
+ initial={{ opacity: 0, x: -50 }}
70
+ animate={{ opacity: 1, x: 0 }}
71
+ className="bg-[#18181b] border-2 border-wedding-pink-500 p-4 rounded-2xl"
72
+ >
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" }) : "--:--:--"}
75
+ </p>
76
+ <p className={`${fontMono.className} text-wedding-gold-400 text-xs uppercase tracking-[0.3em] mt-1 font-bold`}>
77
+ Event Time
78
+ </p>
79
+ </motion.div>
80
+
81
+ <motion.div
82
+ initial={{ opacity: 0, x: 50 }}
83
+ animate={{ opacity: 1, x: 0 }}
84
+ className="bg-white p-3 rounded-2xl flex items-center gap-4 border-2 border-wedding-pink-500"
85
+ >
86
+ <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>
89
+ </div>
90
+ <img src="/pubqr.png" alt="QR Code" className="w-16 h-16 rounded-lg" />
91
+ </motion.div>
92
+ </div>
93
+
94
+ {/* Bottom Bar: Latest News */}
95
+ <AnimatePresence mode="wait">
96
+ {latestUpdate && (
97
+ <motion.div
98
+ key={latestUpdate.id}
99
+ initial={{ opacity: 0, y: 150 }}
100
+ animate={{ opacity: 1, y: 0 }}
101
+ exit={{ opacity: 0, y: 100 }}
102
+ transition={{ type: "spring", damping: 25, stiffness: 120 }}
103
+ className="w-full flex justify-center"
104
+ >
105
+ <div className="relative w-full max-w-5xl">
106
+ {/* Solid outline instead of glow */}
107
+ <div className="absolute -inset-1 border border-wedding-pink-500/20 rounded-[30px]" />
108
+
109
+ <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
+ <div className="flex-shrink-0 flex flex-col items-center gap-2">
112
+ <div className="bg-wedding-pink-500 p-4 rounded-full">
113
+ <HeartFilledIcon className="w-8 h-8 text-white" />
114
+ </div>
115
+ <span className={`${fontMono.className} text-[10px] uppercase tracking-[0.4em] text-wedding-gold-400 font-black`}>
116
+ LIVE
117
+ </span>
118
+ </div>
119
+
120
+ <div className="flex-1 space-y-2">
121
+ <p className={`${fontCursive.className} text-4xl text-wedding-pink-400`}>
122
+ Wedding News
123
+ </p>
124
+ <p className={`${fontSans.className} text-3xl md:text-4xl font-black text-white leading-tight tracking-tight`}>
125
+ {latestUpdate.text}
126
+ </p>
127
+ </div>
128
+
129
+ <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`}>
131
+ Titas & Sukanya
132
+ </p>
133
+ <p className={`${fontMono.className} text-[10px] uppercase tracking-[0.5em] text-zinc-500 font-bold`}>
134
+ Jan 2026
135
+ </p>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </motion.div>
140
+ )}
141
+ </AnimatePresence>
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ getFirestore,
6
+ collection,
7
+ onSnapshot,
8
+ query,
9
+ orderBy,
10
+ } from "firebase/firestore";
11
+ import {
12
+ Spinner,
13
+ Divider,
14
+ Chip,
15
+ Card,
16
+ CardBody,
17
+ CardHeader,
18
+ Avatar,
19
+ Button,
20
+ } from "@heroui/react";
21
+ import { motion, AnimatePresence } from "framer-motion";
22
+
23
+ import firebaseApp from "@/config/firebase";
24
+ import { fontCursive, fontSans, fontMono } from "@/config/fonts";
25
+ import { HeartFilledIcon, ClockIcon } from "@/components/icons";
26
+
27
+ const db = getFirestore(firebaseApp());
28
+
29
+ const formatDateTime = (timestamp) => {
30
+ if (!timestamp?.toDate) return "Just now";
31
+ const date = timestamp.toDate();
32
+ return date.toLocaleDateString("en-IN", {
33
+ day: "numeric",
34
+ month: "short",
35
+ year: "numeric",
36
+ hour: "2-digit",
37
+ minute: "2-digit",
38
+ });
39
+ };
40
+
41
+ const timeAgo = (timestamp) => {
42
+ if (!timestamp?.toDate) return "Just now";
43
+ const now = new Date();
44
+ const diff = (now.getTime() - timestamp.toDate().getTime()) / 1000;
45
+ if (diff < 60) return `${Math.floor(diff)}s ago`;
46
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
47
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
48
+ return `${Math.floor(diff / 86400)}d ago`;
49
+ };
50
+
51
+ const ExpandableText = ({ text, limit = 150 }) => {
52
+ const [expanded, setExpanded] = useState(false);
53
+ const toggle = () => setExpanded(!expanded);
54
+ const isLong = text.length > limit;
55
+ const displayText = expanded || !isLong ? text : text.slice(0, limit) + "...";
56
+
57
+ return (
58
+ <div className="space-y-2">
59
+ <p className={`${fontSans.className} text-default-700 dark:text-gray-200 leading-relaxed text-lg`}>
60
+ {displayText}
61
+ </p>
62
+ {isLong && (
63
+ <Button
64
+ size="sm"
65
+ variant="light"
66
+ color="danger"
67
+ onPress={toggle}
68
+ className="p-0 h-auto min-w-0 font-semibold"
69
+ >
70
+ {expanded ? "Show less" : "Read more"}
71
+ </Button>
72
+ )}
73
+ </div>
74
+ );
75
+ };
76
+
77
+ const UpdateFeed = () => {
78
+ const [updates, setUpdates] = useState([]);
79
+ const [loading, setLoading] = useState(true);
80
+
81
+ useEffect(() => {
82
+ const q = query(collection(db, "updates"), orderBy("createdAt", "desc"));
83
+ const unsubscribe = onSnapshot(q, (snapshot) => {
84
+ const data = snapshot.docs.map((doc) => ({
85
+ id: doc.id,
86
+ ...doc.data(),
87
+ }));
88
+ setUpdates(data);
89
+ setLoading(false);
90
+ });
91
+ return () => unsubscribe();
92
+ }, []);
93
+
94
+ if (loading) {
95
+ return (
96
+ <div className="flex h-[60vh] items-center justify-center">
97
+ <Spinner color="danger" size="lg" label="Loading latest updates..." />
98
+ </div>
99
+ );
100
+ }
101
+
102
+ if (updates.length === 0) {
103
+ return (
104
+ <div className="flex flex-col items-center justify-center h-[60vh] gap-4">
105
+ <HeartFilledIcon className="text-wedding-pink-200 w-16 h-16" />
106
+ <p className={`${fontCursive.className} text-3xl text-default-500`}>
107
+ No updates yet. Stay tuned!
108
+ </p>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ return (
114
+ <div className="max-w-3xl mx-auto px-4 py-12 space-y-12">
115
+ {/* Header */}
116
+ <div className="text-center space-y-4">
117
+ <motion.h1
118
+ initial={{ opacity: 0, y: -20 }}
119
+ animate={{ opacity: 1, y: 0 }}
120
+ className={`${fontCursive.className} text-5xl md:text-7xl text-wedding-pink-600 dark:text-wedding-pink-400`}
121
+ >
122
+ Wedding Updates
123
+ </motion.h1>
124
+ <motion.p
125
+ initial={{ opacity: 0 }}
126
+ animate={{ opacity: 1 }}
127
+ transition={{ delay: 0.2 }}
128
+ className={`${fontMono.className} text-sm uppercase tracking-[0.2em] text-default-500`}
129
+ >
130
+ The latest news from Titas & Sukanya
131
+ </motion.p>
132
+ </div>
133
+
134
+ <div className="space-y-8">
135
+ <AnimatePresence mode="popLayout">
136
+ {updates.map((update, index) => (
137
+ <motion.div
138
+ key={update.id}
139
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
140
+ animate={{ opacity: 1, scale: 1, y: 0 }}
141
+ transition={{ duration: 0.5, delay: index * 0.1 }}
142
+ layout
143
+ >
144
+ <Card
145
+ className={`w-full overflow-hidden border ${
146
+ index === 0
147
+ ? "border-wedding-gold-300 dark:border-wedding-gold-700 bg-wedding-gold-50/50 dark:bg-wedding-gold-900/10 shadow-xl"
148
+ : "border-default-100 dark:border-default-800 bg-white/60 dark:bg-black/40"
149
+ } backdrop-blur-md`}
150
+ >
151
+ {index === 0 && (
152
+ <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-wedding-pink-500 via-wedding-gold-500 to-wedding-pink-500" />
153
+ )}
154
+
155
+ <CardHeader className="flex gap-3 px-6 pt-6">
156
+ <Avatar
157
+ isBordered
158
+ color={index === 0 ? "warning" : "danger"}
159
+ radius="full"
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()}
163
+ />
164
+ <div className="flex flex-col flex-1">
165
+ <div className="flex items-center justify-between">
166
+ <p className="text-md font-bold text-default-800 dark:text-white">
167
+ {update.email === "titas@titas.titas" ? "Titas Mallick" : update.email}
168
+ </p>
169
+ {index === 0 && (
170
+ <Chip
171
+ size="sm"
172
+ variant="shadow"
173
+ color="warning"
174
+ className="animate-pulse bg-wedding-gold-500 text-white"
175
+ >
176
+ LATEST
177
+ </Chip>
178
+ )}
179
+ </div>
180
+ <div className="flex items-center gap-1 text-xs text-default-500">
181
+ <ClockIcon className="w-3 h-3" />
182
+ <span>{formatDateTime(update.createdAt)}</span>
183
+ <span className="mx-1">•</span>
184
+ <span className="font-medium text-wedding-pink-500">{timeAgo(update.createdAt)}</span>
185
+ </div>
186
+ </div>
187
+ </CardHeader>
188
+
189
+ <CardBody className="px-6 py-4">
190
+ <ExpandableText text={update.text} />
191
+ </CardBody>
192
+
193
+ <Divider className="opacity-50" />
194
+
195
+ <div className="px-6 py-3 flex items-center justify-end">
196
+ <HeartFilledIcon className={`w-5 h-5 ${index === 0 ? "text-wedding-pink-500" : "text-wedding-pink-200"}`} />
197
+ </div>
198
+ </Card>
199
+ </motion.div>
200
+ ))}
201
+ </AnimatePresence>
202
+ </div>
203
+ </div>
204
+ );
205
+ };
206
+
207
+ export default UpdateFeed;