@titas_mallick/wedding-site-gen 1.1.0 → 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.
- package/README.md +70 -184
- package/app/api/email-reminders/route.ts +240 -0
- package/app/couple/page.tsx +4 -4
- package/app/game/page.tsx +298 -0
- package/app/guestbook/page.tsx +270 -152
- package/app/invitation/[slug]/layout.tsx +4 -2
- package/app/invitation/[slug]/page.tsx +303 -84
- package/app/invitation/actions.ts +49 -0
- package/app/invitation/maker/auth.js +1 -1
- package/app/invitation/maker/guestAdder.js +4 -0
- package/app/invitation/maker/guestShower.js +39 -8
- package/app/invitation/maker/layout.tsx +1 -1
- package/app/invitation/maker/page.js +9 -7
- package/app/invitation/maker/rsvpViewer.js +90 -8
- package/app/layout.tsx +40 -14
- package/app/mark-the-dates/page.tsx +8 -2
- package/app/page.tsx +7 -1
- package/app/providers.tsx +1 -1
- package/app/sagun/page.tsx +224 -76
- package/app/song-requests/page.tsx +242 -105
- package/app/sukanya/page.tsx +9 -13
- package/app/titas/page.tsx +8 -24
- package/app/travel-guide/page.tsx +361 -120
- package/app/updates/maker/page.js +2 -2
- package/app/updates/overlay/page.tsx +65 -30
- package/app/updates/page.js +3 -3
- package/cli.mjs +26 -15
- package/components/AdminAuth.tsx +145 -0
- package/components/AdminLinks.tsx +120 -0
- package/components/ConciergeBot.tsx +104 -44
- package/components/CountdownTimer.tsx +37 -15
- package/components/Gallery.tsx +1 -1
- package/components/LiveVideos.tsx +27 -15
- package/components/OurStory.tsx +1 -1
- package/components/SchemaMarkup.tsx +74 -0
- package/components/certificate.jsx +287 -300
- package/components/footer.tsx +2 -0
- package/components/hero.tsx +47 -4
- package/components/icons.tsx +45 -0
- package/components/importantNews.js +168 -168
- package/components/navbar.tsx +113 -18
- package/components/updates.tsx +36 -26
- package/config/firebase-admin.js +14 -17
- package/config/firebase.ts +4 -2
- package/config/site.ts +10 -2
- package/firestore.rules +6 -1
- package/next-sitemap.config.js +21 -0
- package/package.json +4 -3
- package/public/corner1-01.svg +0 -0
- package/public/love-birds.png +0 -0
- package/public/next.svg +0 -0
- package/public/pubqr.png +0 -0
- package/public/pw/sample.jpg +0 -0
- package/public/qr.png +0 -0
- package/public/sample.jpg +0 -0
- package/public/vercel.svg +0 -0
- package/vercel.json +1 -0
- package/.recover +0 -9
- package/next-env.d.ts +0 -6
- 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/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/groom.jpg +0 -0
- package/public/invite.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/tsconfig.tsbuildinfo +0 -1
- /package/public/Images/{20.jpg → sample.jpg} +0 -0
package/app/guestbook/page.tsx
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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 =
|
|
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({
|
|
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({
|
|
94
|
+
addToast({
|
|
95
|
+
title: "Deleted",
|
|
96
|
+
description: "Memory removed from gallery.",
|
|
97
|
+
color: "success",
|
|
98
|
+
});
|
|
88
99
|
} catch (err) {
|
|
89
|
-
addToast({
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
306
|
+
✓ {formData.file.name}
|
|
271
307
|
</div>
|
|
272
308
|
) : (
|
|
273
309
|
<>
|
|
274
|
-
<svg
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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">
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
) :
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
{/*
|
|
367
|
-
|
|
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="
|
|
372
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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:
|
|
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
|
|
414
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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">
|
|
453
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
28
|
+
images: ["https://www.titas-sukanya-for.life/invite.jpeg"], // Updated to use absolute URL for WhatsApp
|
|
27
29
|
},
|
|
28
30
|
};
|
|
29
31
|
|