@titas_mallick/wedding-site-gen 1.0.9 → 2.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 (123) hide show
  1. package/README.md +70 -184
  2. package/app/api/email-reminders/route.ts +240 -0
  3. package/app/couple/page.tsx +4 -4
  4. package/app/game/page.tsx +298 -0
  5. package/app/guestbook/page.tsx +270 -152
  6. package/app/invitation/[slug]/layout.tsx +4 -2
  7. package/app/invitation/[slug]/page.tsx +303 -84
  8. package/app/invitation/actions.ts +49 -0
  9. package/app/invitation/maker/auth.js +1 -1
  10. package/app/invitation/maker/guestAdder.js +4 -0
  11. package/app/invitation/maker/guestShower.js +39 -8
  12. package/app/invitation/maker/layout.tsx +1 -1
  13. package/app/invitation/maker/page.js +9 -7
  14. package/app/invitation/maker/rsvpViewer.js +90 -8
  15. package/app/layout.tsx +40 -14
  16. package/app/mark-the-dates/page.tsx +8 -2
  17. package/app/page.tsx +7 -1
  18. package/app/providers.tsx +1 -1
  19. package/app/sagun/page.tsx +224 -76
  20. package/app/song-requests/page.tsx +242 -105
  21. package/app/sukanya/page.tsx +9 -13
  22. package/app/titas/page.tsx +8 -24
  23. package/app/travel-guide/page.tsx +361 -120
  24. package/app/updates/maker/page.js +2 -2
  25. package/app/updates/overlay/page.tsx +65 -30
  26. package/app/updates/page.js +3 -3
  27. package/cli.mjs +49 -21
  28. package/components/AdminAuth.tsx +145 -0
  29. package/components/AdminLinks.tsx +120 -0
  30. package/components/ConciergeBot.tsx +104 -44
  31. package/components/CountdownTimer.tsx +37 -15
  32. package/components/Gallery.tsx +1 -1
  33. package/components/LiveVideos.tsx +27 -15
  34. package/components/OurStory.tsx +1 -1
  35. package/components/SchemaMarkup.tsx +74 -0
  36. package/components/certificate.jsx +287 -300
  37. package/components/footer.tsx +2 -0
  38. package/components/hero.tsx +47 -4
  39. package/components/icons.tsx +45 -0
  40. package/components/importantNews.js +168 -168
  41. package/components/navbar.tsx +113 -18
  42. package/components/updates.tsx +36 -26
  43. package/config/firebase-admin.js +14 -17
  44. package/config/firebase.ts +4 -2
  45. package/config/site.ts +10 -2
  46. package/firestore.rules +6 -1
  47. package/next-sitemap.config.js +21 -0
  48. package/package.json +4 -3
  49. package/public/corner1-01.svg +0 -0
  50. package/public/love-birds.png +0 -0
  51. package/public/next.svg +0 -0
  52. package/public/pubqr.png +0 -0
  53. package/public/pw/sample.jpg +0 -0
  54. package/public/qr.png +0 -0
  55. package/public/sample.jpg +0 -0
  56. package/public/vercel.svg +0 -0
  57. package/vercel.json +1 -0
  58. package/.recover +0 -9
  59. package/next-env.d.ts +0 -6
  60. package/public/DCV.gif +0 -0
  61. package/public/DCV2.gif +0 -0
  62. package/public/DCV3.gif +0 -0
  63. package/public/Images/1.jpg +0 -0
  64. package/public/Images/11.jpg +0 -0
  65. package/public/Images/12.jpg +0 -0
  66. package/public/Images/13.jpg +0 -0
  67. package/public/Images/14.jpg +0 -0
  68. package/public/Images/15.jpg +0 -0
  69. package/public/Images/16.jpg +0 -0
  70. package/public/Images/17.jpg +0 -0
  71. package/public/Images/18.jpg +0 -0
  72. package/public/Images/19.jpg +0 -0
  73. package/public/Images/2.jpg +0 -0
  74. package/public/Images/21.jpg +0 -0
  75. package/public/Images/22.jpg +0 -0
  76. package/public/Images/3.jpg +0 -0
  77. package/public/Images/4.jpg +0 -0
  78. package/public/Images/5.jpg +0 -0
  79. package/public/Images/6.jpg +0 -0
  80. package/public/Images/7.jpg +0 -0
  81. package/public/Images/8.jpg +0 -0
  82. package/public/Images/9.jpg +0 -0
  83. package/public/Images/9b.jpg +0 -0
  84. package/public/Images/Patipatra.jpeg +0 -0
  85. package/public/audio (1).mp3 +0 -0
  86. package/public/audio (2).mp3 +0 -0
  87. package/public/bride.jpg +0 -0
  88. package/public/groom.jpg +0 -0
  89. package/public/invite.png +0 -0
  90. package/public/pw/001.jpg +0 -0
  91. package/public/pw/002.jpg +0 -0
  92. package/public/pw/003.jpg +0 -0
  93. package/public/pw/004.jpg +0 -0
  94. package/public/pw/005.jpg +0 -0
  95. package/public/pw/006.jpg +0 -0
  96. package/public/pw/007.jpg +0 -0
  97. package/public/pw/008.jpg +0 -0
  98. package/public/pw/009.jpg +0 -0
  99. package/public/pw/010.jpg +0 -0
  100. package/public/pw/011.jpg +0 -0
  101. package/public/pw/012.jpg +0 -0
  102. package/public/pw/013.jpg +0 -0
  103. package/public/pw/014.jpg +0 -0
  104. package/public/pw/015.jpg +0 -0
  105. package/public/pw/016.jpg +0 -0
  106. package/public/pw/017.jpg +0 -0
  107. package/public/pw/018.jpg +0 -0
  108. package/public/pw/019.jpg +0 -0
  109. package/public/pw/020.jpg +0 -0
  110. package/public/pw/021.jpg +0 -0
  111. package/public/pw/022.jpg +0 -0
  112. package/public/pw/023.jpg +0 -0
  113. package/public/pw/024.jpg +0 -0
  114. package/public/pw/025.jpg +0 -0
  115. package/public/pw/026.jpg +0 -0
  116. package/public/pw/027.jpg +0 -0
  117. package/public/pw/028.jpg +0 -0
  118. package/public/pw/029.jpg +0 -0
  119. package/public/pw/030.jpg +0 -0
  120. package/public/pw/031.jpg +0 -0
  121. package/public/pw/032.jpg +0 -0
  122. package/tsconfig.tsbuildinfo +0 -1
  123. /package/public/Images/{20.jpg → sample.jpg} +0 -0
@@ -2,14 +2,13 @@
2
2
 
3
3
  import { useState, useEffect } from "react";
4
4
  import { motion, AnimatePresence } from "framer-motion";
5
- import {
6
- Card,
7
- CardBody,
8
- CardFooter,
9
- Button,
10
- Input,
11
- Image,
12
- addToast,
5
+ import {
6
+ Card,
7
+ CardBody,
8
+ Button,
9
+ Input,
10
+ Image,
11
+ addToast,
13
12
  Spinner,
14
13
  Divider,
15
14
  Avatar,
@@ -17,30 +16,31 @@ import {
17
16
  ModalContent,
18
17
  ModalBody,
19
18
  ModalFooter,
20
- useDisclosure
19
+ useDisclosure,
21
20
  } from "@heroui/react";
22
- import {
23
- getFirestore,
24
- collection,
25
- addDoc,
26
- onSnapshot,
27
- query,
28
- orderBy,
21
+ import {
22
+ getFirestore,
23
+ collection,
24
+ addDoc,
25
+ onSnapshot,
26
+ query,
27
+ orderBy,
29
28
  serverTimestamp,
30
29
  deleteDoc,
31
- doc
30
+ doc,
32
31
  } from "firebase/firestore";
33
32
  import { getAuth, onAuthStateChanged } from "firebase/auth";
33
+
34
34
  import firebaseApp from "@/config/firebase";
35
- import { fontCursive, fontSans, fontMono } from "@/config/fonts";
36
- import { HeartFilledIcon } from "@/components/icons";
35
+ import { fontCursive, fontSans } from "@/config/fonts";
37
36
 
38
37
  const db = getFirestore(firebaseApp());
39
38
  const auth = getAuth(firebaseApp());
40
39
 
41
40
  // --- CLOUDINARY CONFIGURATION ---
42
- const CLOUDINARY_CLOUD_NAME = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
43
- const CLOUDINARY_UPLOAD_PRESET = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
41
+ const CLOUDINARY_CLOUD_NAME = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
42
+ const CLOUDINARY_UPLOAD_PRESET =
43
+ process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET;
44
44
  const CLOUDINARY_URL = `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`;
45
45
  // ---------------------------------
46
46
 
@@ -48,9 +48,11 @@ const timeAgo = (timestamp: any) => {
48
48
  if (!timestamp?.toDate) return "Just now";
49
49
  const now = new Date();
50
50
  const diff = (now.getTime() - timestamp.toDate().getTime()) / 1000;
51
+
51
52
  if (diff < 60) return `${Math.floor(diff)}s ago`;
52
53
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
53
54
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
55
+
54
56
  return `${Math.floor(diff / 86400)}d ago`;
55
57
  };
56
58
 
@@ -59,11 +61,15 @@ export default function GuestbookPage() {
59
61
  const [loading, setLoading] = useState(true);
60
62
  const [uploading, setUploading] = useState(false);
61
63
  const [user, setUser] = useState<any>(null);
62
- const [formData, setFormData] = useState({ name: "", file: null as File | null, captcha: "" });
63
-
64
+ const [formData, setFormData] = useState({
65
+ name: "",
66
+ file: null as File | null,
67
+ captcha: "",
68
+ });
69
+
64
70
  const { isOpen, onOpen, onOpenChange } = useDisclosure();
65
71
  const [selectedImage, setSelectedImage] = useState<any>(null);
66
-
72
+
67
73
  // Delete Confirmation States
68
74
  const [imageToDelete, setImageToDelete] = useState<string | null>(null);
69
75
  const [isConfirmOpen, setIsConfirmOpen] = useState(false);
@@ -72,6 +78,7 @@ export default function GuestbookPage() {
72
78
  const unsubscribeAuth = onAuthStateChanged(auth, (currentUser) => {
73
79
  setUser(currentUser);
74
80
  });
81
+
75
82
  return () => unsubscribeAuth();
76
83
  }, []);
77
84
 
@@ -84,9 +91,17 @@ export default function GuestbookPage() {
84
91
  if (!imageToDelete) return;
85
92
  try {
86
93
  await deleteDoc(doc(db, "guestbook", imageToDelete));
87
- addToast({ title: "Deleted", description: "Memory removed from gallery.", color: "success" });
94
+ addToast({
95
+ title: "Deleted",
96
+ description: "Memory removed from gallery.",
97
+ color: "success",
98
+ });
88
99
  } catch (err) {
89
- addToast({ title: "Error", description: "Failed to delete.", color: "danger" });
100
+ addToast({
101
+ title: "Error",
102
+ description: "Failed to delete.",
103
+ color: "danger",
104
+ });
90
105
  } finally {
91
106
  setIsConfirmOpen(false);
92
107
  setImageToDelete(null);
@@ -96,9 +111,11 @@ export default function GuestbookPage() {
96
111
  const resizeImage = (file: File): Promise<Blob> => {
97
112
  return new Promise((resolve) => {
98
113
  const reader = new FileReader();
114
+
99
115
  reader.readAsDataURL(file);
100
116
  reader.onload = (event) => {
101
117
  const img = new window.Image();
118
+
102
119
  img.src = event.target?.result as string;
103
120
  img.onload = () => {
104
121
  const canvas = document.createElement("canvas");
@@ -121,13 +138,14 @@ export default function GuestbookPage() {
121
138
  canvas.width = width;
122
139
  canvas.height = height;
123
140
  const ctx = canvas.getContext("2d");
141
+
124
142
  ctx?.drawImage(img, 0, 0, width, height);
125
143
  canvas.toBlob(
126
144
  (blob) => {
127
145
  if (blob) resolve(blob);
128
146
  },
129
147
  "image/jpeg",
130
- 0.7 // Compression quality
148
+ 0.7, // Compression quality
131
149
  );
132
150
  };
133
151
  };
@@ -142,10 +160,12 @@ export default function GuestbookPage() {
142
160
  useEffect(() => {
143
161
  const q = query(collection(db, "guestbook"), orderBy("createdAt", "desc"));
144
162
  const unsubscribe = onSnapshot(q, (snapshot) => {
145
- const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
163
+ const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
164
+
146
165
  setPhotos(data);
147
166
  setLoading(false);
148
167
  });
168
+
149
169
  return () => unsubscribe();
150
170
  }, []);
151
171
 
@@ -166,6 +186,7 @@ export default function GuestbookPage() {
166
186
  description: "Please answer the question correctly (Hint: 10 years!).",
167
187
  color: "warning",
168
188
  });
189
+
169
190
  return;
170
191
  }
171
192
 
@@ -173,9 +194,10 @@ export default function GuestbookPage() {
173
194
  try {
174
195
  // 1. Resize and compress image
175
196
  const resizedBlob = await resizeImage(formData.file);
176
-
197
+
177
198
  // 2. Upload to Cloudinary
178
199
  const data = new FormData();
200
+
179
201
  data.append("file", resizedBlob, "upload.jpg");
180
202
  data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!);
181
203
 
@@ -217,93 +239,123 @@ export default function GuestbookPage() {
217
239
  {/* Header */}
218
240
  <section className="text-center mb-16 space-y-4">
219
241
  <motion.div
220
- initial={{ opacity: 0, scale: 0.9 }}
221
242
  animate={{ opacity: 1, scale: 1 }}
222
243
  className="bg-wedding-pink-50 dark:bg-wedding-pink-900/20 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6"
244
+ initial={{ opacity: 0, scale: 0.9 }}
223
245
  >
224
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-wedding-pink-500">
225
- <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
226
- <circle cx="12" cy="13" r="4"></circle>
227
- </svg>
246
+ <svg
247
+ className="text-wedding-pink-500"
248
+ fill="none"
249
+ height="32"
250
+ stroke="currentColor"
251
+ strokeLinecap="round"
252
+ strokeLinejoin="round"
253
+ strokeWidth="2"
254
+ viewBox="0 0 24 24"
255
+ width="32"
256
+ >
257
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
258
+ <circle cx="12" cy="13" r="4" />
259
+ </svg>
228
260
  </motion.div>
229
- <h1 className={`${fontCursive.className} text-5xl md:text-7xl text-wedding-pink-600 dark:text-wedding-pink-400 py-2`}>
261
+ <h1
262
+ className={`${fontCursive.className} text-5xl md:text-7xl text-wedding-pink-600 dark:text-wedding-pink-400 py-2`}
263
+ >
230
264
  Digital Guestbook
231
265
  </h1>
232
266
  <p className="text-default-500 max-w-xl mx-auto">
233
- Capture a moment and share it with us! Upload a selfie or a memory to be part of our wedding gallery forever.
267
+ Capture a moment and share it with us! Upload a selfie or a memory to
268
+ be part of our wedding gallery forever.
234
269
  </p>
235
270
  </section>
236
271
 
237
272
  {/* Upload Section */}
238
273
  <Card className="max-w-xl mx-auto mb-20 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl border border-wedding-gold-200 dark:border-wedding-gold-800 p-8 rounded-[40px] shadow-2xl">
239
- <form onSubmit={handleUpload} className="space-y-6">
274
+ <form className="space-y-6" onSubmit={handleUpload}>
240
275
  <Input
241
276
  isRequired
277
+ classNames={{
278
+ label: "text-wedding-pink-600 font-bold",
279
+ input: "outline-none",
280
+ inputWrapper:
281
+ "border-wedding-pink-100 focus-within:!border-wedding-pink-500 h-14",
282
+ }}
242
283
  label="Your Name"
243
284
  labelPlacement="outside-top"
244
285
  placeholder="Who's behind the camera?"
245
- variant="bordered"
246
286
  value={formData.name}
287
+ variant="bordered"
247
288
  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
248
- classNames={{
249
- label: "text-wedding-pink-600 font-bold",
250
- input: "outline-none",
251
- inputWrapper: "border-wedding-pink-100 focus-within:!border-wedding-pink-500 h-14"
252
- }}
253
289
  />
254
-
290
+
255
291
  <div className="relative">
256
- <input
257
- type="file"
258
- accept="image/*"
292
+ <input
293
+ accept="image/*"
294
+ className="hidden"
295
+ id="photo-upload"
259
296
  multiple={false}
297
+ type="file"
260
298
  onChange={handleFileChange}
261
- className="hidden"
262
- id="photo-upload"
263
299
  />
264
- <label
265
- htmlFor="photo-upload"
300
+ <label
266
301
  className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-wedding-pink-200 dark:border-wedding-pink-900/30 rounded-2xl cursor-pointer hover:bg-wedding-pink-50 dark:hover:bg-wedding-pink-900/10 transition-colors"
302
+ htmlFor="photo-upload"
267
303
  >
268
304
  {formData.file ? (
269
305
  <div className="text-center text-wedding-pink-600 font-bold">
270
- ✓ {formData.file.name}
306
+ ✓ {formData.file.name}
271
307
  </div>
272
308
  ) : (
273
309
  <>
274
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-wedding-pink-400 mb-2">
275
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
276
- <polyline points="17 8 12 3 7 8"></polyline>
277
- <line x1="12" y1="3" x2="12" y2="15"></line>
310
+ <svg
311
+ className="text-wedding-pink-400 mb-2"
312
+ fill="none"
313
+ height="24"
314
+ stroke="currentColor"
315
+ strokeLinecap="round"
316
+ strokeLinejoin="round"
317
+ strokeWidth="2"
318
+ viewBox="0 0 24 24"
319
+ width="24"
320
+ >
321
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
322
+ <polyline points="17 8 12 3 7 8" />
323
+ <line x1="12" x2="12" y1="3" y2="15" />
278
324
  </svg>
279
- <span className="text-xs text-default-500 font-medium">Select a Photo to Upload</span>
325
+ <span className="text-xs text-default-500 font-medium">
326
+ Select a Photo to Upload
327
+ </span>
280
328
  </>
281
329
  )}
282
330
  </label>
283
331
  </div>
284
332
 
285
333
  <div className="p-4 bg-wedding-pink-50 dark:bg-wedding-pink-900/10 rounded-2xl border border-wedding-pink-100 dark:border-wedding-pink-800/30">
286
- <p className="text-xs font-bold text-wedding-pink-600 dark:text-wedding-pink-400 uppercase tracking-widest mb-2">Bot Protection</p>
287
- <Input
334
+ <p className="text-xs font-bold text-wedding-pink-600 dark:text-wedding-pink-400 uppercase tracking-widest mb-2">
335
+ Bot Protection
336
+ </p>
337
+ <Input
288
338
  isRequired
289
- label="How many years have we been together?"
290
- labelPlacement="outside-top"
291
- placeholder="Answer in numbers"
292
- variant="underlined"
293
- value={formData.captcha}
294
- onChange={(e) => setFormData({ ...formData, captcha: e.target.value })}
295
339
  classNames={{
296
340
  label: "text-default-600 text-xs",
297
341
  input: "text-center font-bold text-xl outline-none",
298
342
  }}
343
+ label="How many years have we been together?"
344
+ labelPlacement="outside-top"
345
+ placeholder="Answer in numbers"
346
+ value={formData.captcha}
347
+ variant="underlined"
348
+ onChange={(e) =>
349
+ setFormData({ ...formData, captcha: e.target.value })
350
+ }
299
351
  />
300
352
  </div>
301
353
 
302
- <Button
303
- type="submit"
304
- isLoading={uploading}
305
- isDisabled={!formData.file || !formData.name}
354
+ <Button
306
355
  className="w-full bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 text-white font-black h-14 text-lg rounded-full"
356
+ isDisabled={!formData.file || !formData.name}
357
+ isLoading={uploading}
358
+ type="submit"
307
359
  >
308
360
  Share Memory
309
361
  </Button>
@@ -319,109 +371,153 @@ export default function GuestbookPage() {
319
371
  <div className="col-span-full flex justify-center py-20">
320
372
  <Spinner color="danger" />
321
373
  </div>
322
- ) : photos.map((photo, i) => (
323
- <motion.div
324
- key={photo.id}
325
- initial={{ opacity: 0, scale: 0.9 }}
326
- animate={{ opacity: 1, scale: 1 }}
327
- transition={{ delay: i * 0.05 }}
328
- className="break-inside-avoid"
329
- >
330
- <Card
331
- className="border-none shadow-md bg-white dark:bg-zinc-900 rounded-[24px] overflow-hidden relative cursor-pointer group active:scale-[0.98] transition-transform"
332
- onClick={(e) => {
333
- // Only open lightbox if NOT clicking the delete button
334
- if (!(e.target as HTMLElement).closest('.delete-btn')) {
335
- openLightbox(photo);
336
- }
337
- }}
374
+ ) : (
375
+ photos.map((photo, i) => (
376
+ <motion.div
377
+ key={photo.id}
378
+ animate={{ opacity: 1, scale: 1 }}
379
+ className="break-inside-avoid"
380
+ initial={{ opacity: 0, scale: 0.9 }}
381
+ transition={{ delay: i * 0.05 }}
338
382
  >
339
- <CardBody className="p-0">
340
- <Image
341
- removeWrapper
342
- alt={`Memory by ${photo.name}`}
343
- className="w-full h-auto object-cover z-0"
344
- src={photo.imageUrl}
345
- radius="none"
346
- />
347
-
348
- {/* Info Overlay */}
349
- <div className="absolute bottom-0 left-0 w-full p-4 bg-gradient-to-t from-black/90 via-black/40 to-transparent z-10 flex items-end justify-between">
350
- <div className="flex flex-col gap-1.5">
351
- <div className="flex items-center gap-2">
352
- <Avatar
353
- name={photo.name}
354
- size="sm"
355
- className="w-6 h-6 text-[10px] bg-wedding-pink-500 text-white font-bold"
356
- />
357
- <p className="text-white text-xs font-black tracking-tight truncate">
358
- {photo.name}
359
- </p>
360
- </div>
361
- <p className="text-white/80 text-[9px] font-mono ml-8 uppercase tracking-wider">
362
- {timeAgo(photo.createdAt)}
363
- </p>
364
- </div>
383
+ <Card
384
+ className="border-none shadow-md bg-white dark:bg-zinc-900 rounded-[24px] overflow-hidden relative cursor-pointer group active:scale-[0.98] transition-transform"
385
+ onClick={(e) => {
386
+ // Only open lightbox if NOT clicking the delete button
387
+ if (!(e.target as HTMLElement).closest(".delete-btn")) {
388
+ openLightbox(photo);
389
+ }
390
+ }}
391
+ >
392
+ <CardBody className="p-0">
393
+ <Image
394
+ removeWrapper
395
+ alt={`Memory by ${photo.name}`}
396
+ className="w-full h-auto object-cover z-0"
397
+ radius="none"
398
+ src={photo.imageUrl}
399
+ />
365
400
 
366
- {/* Admin Delete Button */}
367
- {user?.email === "titas@titas.titas" && (
401
+ {/* Expand Icon Indicator */}
402
+ <div className="absolute top-3 right-3 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
368
403
  <Button
369
404
  isIconOnly
405
+ className="bg-black/60 backdrop-blur-md rounded-full border border-white/20 text-white min-w-0 h-8 w-8"
370
406
  size="sm"
371
- variant="light"
372
- color="danger"
373
- className="min-w-0 p-0 h-auto delete-btn"
374
- onClick={() => handleDeleteClick(photo.id)}
407
+ variant="flat"
408
+ onPress={() => openLightbox(photo)}
375
409
  >
376
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
377
- <polyline points="3 6 5 3 21 6"></polyline>
378
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
410
+ <svg
411
+ fill="none"
412
+ height="16"
413
+ stroke="currentColor"
414
+ strokeLinecap="round"
415
+ strokeLinejoin="round"
416
+ strokeWidth="2.5"
417
+ viewBox="0 0 24 24"
418
+ width="16"
419
+ >
420
+ <polyline points="15 3 21 3 21 9" />
421
+ <polyline points="9 21 3 21 3 15" />
422
+ <line x1="21" x2="14" y1="3" y2="10" />
423
+ <line x1="3" x2="10" y1="21" y2="14" />
379
424
  </svg>
380
425
  </Button>
381
- )}
382
- </div>
383
- </CardBody>
384
- </Card>
385
- </motion.div>
386
- ))}
426
+ </div>
427
+
428
+ {/* Info Overlay */}
429
+ <div className="absolute bottom-0 left-0 w-full p-4 bg-gradient-to-t from-black/90 via-black/40 to-transparent z-10 flex items-end justify-between">
430
+ <div className="flex flex-col gap-1.5">
431
+ <div className="flex items-center gap-2">
432
+ <Avatar
433
+ className="w-6 h-6 text-[10px] bg-wedding-pink-500 text-white font-bold"
434
+ name={photo.name}
435
+ size="sm"
436
+ />
437
+ <p className="text-white text-xs font-black tracking-tight truncate">
438
+ {photo.name}
439
+ </p>
440
+ </div>
441
+ <p className="text-white/80 text-[9px] font-mono ml-8 uppercase tracking-wider">
442
+ {timeAgo(photo.createdAt)}
443
+ </p>
444
+ </div>
445
+
446
+ {/* Admin Delete Button */}
447
+ {user?.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL && (
448
+ <Button
449
+ isIconOnly
450
+ className="min-w-0 p-0 h-auto delete-btn"
451
+ color="danger"
452
+ size="sm"
453
+ variant="light"
454
+ onClick={(e) => e.stopPropagation()}
455
+ onPress={(e) => {
456
+ e.continuePropagation(); // HeroUI specific if needed, but standard stopPropagation is safer for native elements
457
+ handleDeleteClick(photo.id);
458
+ }}
459
+ >
460
+ <svg
461
+ fill="none"
462
+ height="14"
463
+ stroke="currentColor"
464
+ strokeLinecap="round"
465
+ strokeLinejoin="round"
466
+ strokeWidth="2.5"
467
+ viewBox="0 0 24 24"
468
+ width="14"
469
+ >
470
+ <polyline points="3 6 5 3 21 6" />
471
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
472
+ </svg>
473
+ </Button>
474
+ )}
475
+ </div>
476
+ </CardBody>
477
+ </Card>
478
+ </motion.div>
479
+ ))
480
+ )}
387
481
  </AnimatePresence>
388
482
  </div>
389
483
 
390
484
  {/* Lightbox Modal */}
391
- <Modal
392
- isOpen={isOpen}
393
- onOpenChange={onOpenChange}
485
+ <Modal
394
486
  backdrop="blur"
395
- size="full"
396
487
  classNames={{
397
488
  wrapper: "z-[200]",
398
489
  backdrop: "bg-black/80 backdrop-blur-md",
399
490
  base: "bg-transparent shadow-none border-none",
400
- closeButton: "hover:bg-white/20 active:bg-white/10 text-white text-2xl z-50",
491
+ closeButton:
492
+ "hover:bg-white/20 active:bg-white/10 text-white text-2xl z-50",
401
493
  }}
494
+ isOpen={isOpen}
495
+ size="full"
496
+ onOpenChange={onOpenChange}
402
497
  >
403
498
  <ModalContent>
404
499
  {(onClose) => (
405
- <ModalBody
500
+ <ModalBody
406
501
  className="p-4 flex items-center justify-center h-screen w-screen overflow-hidden cursor-pointer"
407
502
  onClick={onClose}
408
503
  >
409
504
  {selectedImage && (
410
- <motion.div
411
- initial={{ opacity: 0, scale: 0.9 }}
505
+ <motion.div
412
506
  animate={{ opacity: 1, scale: 1 }}
413
- className="relative flex flex-col items-center max-w-full max-h-full cursor-default"
414
- onClick={(e) => e.stopPropagation()}
507
+ className="relative flex flex-col items-center max-w-full max-h-full"
508
+ initial={{ opacity: 0, scale: 0.9 }}
415
509
  >
416
510
  <Image
417
511
  alt={`Memory by ${selectedImage.name}`}
418
- src={selectedImage.imageUrl}
419
512
  className="max-h-[80vh] w-auto object-contain rounded-2xl shadow-2xl"
513
+ src={selectedImage.imageUrl}
420
514
  />
421
515
  <div className="mt-6 bg-black/60 backdrop-blur-md px-8 py-3 rounded-full border border-white/20">
422
- <p className={`${fontSans.className} text-white text-base md:text-lg font-bold`}>
423
- Memory by {selectedImage.name}
424
- </p>
516
+ <p
517
+ className={`${fontSans.className} text-white text-base md:text-lg font-bold`}
518
+ >
519
+ Memory by {selectedImage.name}
520
+ </p>
425
521
  </div>
426
522
  </motion.div>
427
523
  )}
@@ -431,33 +527,55 @@ export default function GuestbookPage() {
431
527
  </Modal>
432
528
 
433
529
  {/* Delete Confirmation Modal */}
434
- <Modal
435
- isOpen={isConfirmOpen}
436
- onOpenChange={setIsConfirmOpen}
530
+ <Modal
437
531
  backdrop="blur"
438
- size="xs"
439
532
  className="dark:bg-zinc-900 border border-default-100 dark:border-zinc-800"
533
+ isOpen={isConfirmOpen}
534
+ size="xs"
535
+ onOpenChange={setIsConfirmOpen}
440
536
  >
441
537
  <ModalContent>
442
538
  {(onClose) => (
443
539
  <>
444
540
  <ModalBody className="pt-8 pb-4 text-center space-y-4">
445
541
  <div className="w-16 h-16 bg-danger-50 dark:bg-danger-900/20 rounded-full flex items-center justify-center mx-auto text-danger animate-bounce">
446
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
447
- <polyline points="3 6 5 6 21 6"></polyline>
448
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
449
- </svg>
542
+ <svg
543
+ fill="none"
544
+ height="32"
545
+ stroke="currentColor"
546
+ strokeLinecap="round"
547
+ strokeLinejoin="round"
548
+ strokeWidth="2"
549
+ viewBox="0 0 24 24"
550
+ width="32"
551
+ >
552
+ <polyline points="3 6 5 6 21 6" />
553
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
554
+ </svg>
450
555
  </div>
451
556
  <div>
452
- <h3 className="text-xl font-bold text-default-900 dark:text-white">Delete Memory?</h3>
453
- <p className="text-sm text-default-500 mt-2">This action cannot be undone. This memory will be gone forever.</p>
557
+ <h3 className="text-xl font-bold text-default-900 dark:text-white">
558
+ Delete Memory?
559
+ </h3>
560
+ <p className="text-sm text-default-500 mt-2">
561
+ This action cannot be undone. This memory will be gone
562
+ forever.
563
+ </p>
454
564
  </div>
455
565
  </ModalBody>
456
566
  <ModalFooter className="flex-col gap-2 pb-8">
457
- <Button color="danger" className="w-full font-bold h-12" onPress={handleConfirmDelete}>
567
+ <Button
568
+ className="w-full font-bold h-12"
569
+ color="danger"
570
+ onPress={handleConfirmDelete}
571
+ >
458
572
  Yes, Delete Forever
459
573
  </Button>
460
- <Button variant="flat" className="w-full font-semibold h-12" onPress={onClose}>
574
+ <Button
575
+ className="w-full font-semibold h-12"
576
+ variant="flat"
577
+ onPress={onClose}
578
+ >
461
579
  Cancel
462
580
  </Button>
463
581
  </ModalFooter>
@@ -9,10 +9,12 @@ export const metadata: Metadata = {
9
9
  description:
10
10
  "We would be honored to have you join us as we celebrate our wedding. View details for the ceremony and reception.",
11
11
  url: "https://www.titas-sukanya-for.life/invitation",
12
+ siteName: "Titas & Sukanya",
13
+ locale: "en_US",
12
14
  type: "website",
13
15
  images: [
14
16
  {
15
- url: "/invite.png", // Updated to use invite.png from public root
17
+ url: "https://www.titas-sukanya-for.life/invite.jpeg", // Updated to use absolute URL for WhatsApp
16
18
  width: 1200,
17
19
  height: 630,
18
20
  alt: "Titas & Sukanya Wedding Invitation",
@@ -23,7 +25,7 @@ export const metadata: Metadata = {
23
25
  card: "summary_large_image",
24
26
  title: "Wedding Invitation | Titas & Sukanya",
25
27
  description: "Join us in celebrating our special day.",
26
- images: ["/invite.png"], // Updated to use invite.png from public root
28
+ images: ["https://www.titas-sukanya-for.life/invite.jpeg"], // Updated to use absolute URL for WhatsApp
27
29
  },
28
30
  };
29
31