@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,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;
|