@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
package/app/error.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
export default function Error({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Log the error to an error reporting service
|
|
14
|
+
/* eslint-disable no-console */
|
|
15
|
+
console.error(error);
|
|
16
|
+
}, [error]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<h2>Something went wrong!</h2>
|
|
21
|
+
<button
|
|
22
|
+
onClick={
|
|
23
|
+
// Attempt to recover by trying to re-render the segment
|
|
24
|
+
() => reset()
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
Try again
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardBody,
|
|
8
|
+
CardFooter,
|
|
9
|
+
Button,
|
|
10
|
+
Input,
|
|
11
|
+
Image,
|
|
12
|
+
addToast,
|
|
13
|
+
Spinner,
|
|
14
|
+
Divider,
|
|
15
|
+
Avatar,
|
|
16
|
+
Modal,
|
|
17
|
+
ModalContent,
|
|
18
|
+
ModalBody,
|
|
19
|
+
ModalFooter,
|
|
20
|
+
useDisclosure
|
|
21
|
+
} from "@heroui/react";
|
|
22
|
+
import {
|
|
23
|
+
getFirestore,
|
|
24
|
+
collection,
|
|
25
|
+
addDoc,
|
|
26
|
+
onSnapshot,
|
|
27
|
+
query,
|
|
28
|
+
orderBy,
|
|
29
|
+
serverTimestamp,
|
|
30
|
+
deleteDoc,
|
|
31
|
+
doc
|
|
32
|
+
} from "firebase/firestore";
|
|
33
|
+
import { getAuth, onAuthStateChanged } from "firebase/auth";
|
|
34
|
+
import firebaseApp from "@/config/firebase";
|
|
35
|
+
import { fontCursive, fontSans, fontMono } from "@/config/fonts";
|
|
36
|
+
import { HeartFilledIcon } from "@/components/icons";
|
|
37
|
+
|
|
38
|
+
const db = getFirestore(firebaseApp());
|
|
39
|
+
const auth = getAuth(firebaseApp());
|
|
40
|
+
|
|
41
|
+
// --- 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;
|
|
44
|
+
const CLOUDINARY_URL = `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`;
|
|
45
|
+
// ---------------------------------
|
|
46
|
+
|
|
47
|
+
const timeAgo = (timestamp: any) => {
|
|
48
|
+
if (!timestamp?.toDate) return "Just now";
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const diff = (now.getTime() - timestamp.toDate().getTime()) / 1000;
|
|
51
|
+
if (diff < 60) return `${Math.floor(diff)}s ago`;
|
|
52
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
53
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
54
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default function GuestbookPage() {
|
|
58
|
+
const [photos, setPhotos] = useState<any[]>([]);
|
|
59
|
+
const [loading, setLoading] = useState(true);
|
|
60
|
+
const [uploading, setUploading] = useState(false);
|
|
61
|
+
const [user, setUser] = useState<any>(null);
|
|
62
|
+
const [formData, setFormData] = useState({ name: "", file: null as File | null, captcha: "" });
|
|
63
|
+
|
|
64
|
+
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
|
65
|
+
const [selectedImage, setSelectedImage] = useState<any>(null);
|
|
66
|
+
|
|
67
|
+
// Delete Confirmation States
|
|
68
|
+
const [imageToDelete, setImageToDelete] = useState<string | null>(null);
|
|
69
|
+
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const unsubscribeAuth = onAuthStateChanged(auth, (currentUser) => {
|
|
73
|
+
setUser(currentUser);
|
|
74
|
+
});
|
|
75
|
+
return () => unsubscribeAuth();
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleDeleteClick = (id: string) => {
|
|
79
|
+
setImageToDelete(id);
|
|
80
|
+
setIsConfirmOpen(true);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleConfirmDelete = async () => {
|
|
84
|
+
if (!imageToDelete) return;
|
|
85
|
+
try {
|
|
86
|
+
await deleteDoc(doc(db, "guestbook", imageToDelete));
|
|
87
|
+
addToast({ title: "Deleted", description: "Memory removed from gallery.", color: "success" });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
addToast({ title: "Error", description: "Failed to delete.", color: "danger" });
|
|
90
|
+
} finally {
|
|
91
|
+
setIsConfirmOpen(false);
|
|
92
|
+
setImageToDelete(null);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resizeImage = (file: File): Promise<Blob> => {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const reader = new FileReader();
|
|
99
|
+
reader.readAsDataURL(file);
|
|
100
|
+
reader.onload = (event) => {
|
|
101
|
+
const img = new window.Image();
|
|
102
|
+
img.src = event.target?.result as string;
|
|
103
|
+
img.onload = () => {
|
|
104
|
+
const canvas = document.createElement("canvas");
|
|
105
|
+
const MAX_WIDTH = 1200;
|
|
106
|
+
const MAX_HEIGHT = 1200;
|
|
107
|
+
let width = img.width;
|
|
108
|
+
let height = img.height;
|
|
109
|
+
|
|
110
|
+
if (width > height) {
|
|
111
|
+
if (width > MAX_WIDTH) {
|
|
112
|
+
height *= MAX_WIDTH / width;
|
|
113
|
+
width = MAX_WIDTH;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
if (height > MAX_HEIGHT) {
|
|
117
|
+
width *= MAX_HEIGHT / height;
|
|
118
|
+
height = MAX_HEIGHT;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
canvas.width = width;
|
|
122
|
+
canvas.height = height;
|
|
123
|
+
const ctx = canvas.getContext("2d");
|
|
124
|
+
ctx?.drawImage(img, 0, 0, width, height);
|
|
125
|
+
canvas.toBlob(
|
|
126
|
+
(blob) => {
|
|
127
|
+
if (blob) resolve(blob);
|
|
128
|
+
},
|
|
129
|
+
"image/jpeg",
|
|
130
|
+
0.7 // Compression quality
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const openLightbox = (image: any) => {
|
|
138
|
+
setSelectedImage(image);
|
|
139
|
+
onOpen();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const q = query(collection(db, "guestbook"), orderBy("createdAt", "desc"));
|
|
144
|
+
const unsubscribe = onSnapshot(q, (snapshot) => {
|
|
145
|
+
const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
|
146
|
+
setPhotos(data);
|
|
147
|
+
setLoading(false);
|
|
148
|
+
});
|
|
149
|
+
return () => unsubscribe();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
153
|
+
if (e.target.files && e.target.files[0]) {
|
|
154
|
+
setFormData({ ...formData, file: e.target.files[0] });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleUpload = async (e: React.FormEvent) => {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
if (!formData.file || !formData.name) return;
|
|
161
|
+
|
|
162
|
+
// Bot Protection
|
|
163
|
+
if (formData.captcha.trim() !== "10") {
|
|
164
|
+
addToast({
|
|
165
|
+
title: "Security Check",
|
|
166
|
+
description: "Please answer the question correctly (Hint: 10 years!).",
|
|
167
|
+
color: "warning",
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setUploading(true);
|
|
173
|
+
try {
|
|
174
|
+
// 1. Resize and compress image
|
|
175
|
+
const resizedBlob = await resizeImage(formData.file);
|
|
176
|
+
|
|
177
|
+
// 2. Upload to Cloudinary
|
|
178
|
+
const data = new FormData();
|
|
179
|
+
data.append("file", resizedBlob, "upload.jpg");
|
|
180
|
+
data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!);
|
|
181
|
+
|
|
182
|
+
const res = await fetch(CLOUDINARY_URL, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
body: data,
|
|
185
|
+
});
|
|
186
|
+
const file = await res.json();
|
|
187
|
+
|
|
188
|
+
if (file.secure_url) {
|
|
189
|
+
// 2. Save URL to Firestore
|
|
190
|
+
await addDoc(collection(db, "guestbook"), {
|
|
191
|
+
name: formData.name,
|
|
192
|
+
imageUrl: file.secure_url,
|
|
193
|
+
createdAt: serverTimestamp(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
addToast({
|
|
197
|
+
title: "Memory Shared!",
|
|
198
|
+
description: "Your photo has been added to our guestbook.",
|
|
199
|
+
color: "success",
|
|
200
|
+
});
|
|
201
|
+
setFormData({ name: "", file: null, captcha: "" });
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error("Upload error", err);
|
|
205
|
+
addToast({
|
|
206
|
+
title: "Upload Failed",
|
|
207
|
+
description: "Something went wrong. Please try again.",
|
|
208
|
+
color: "danger",
|
|
209
|
+
});
|
|
210
|
+
} finally {
|
|
211
|
+
setUploading(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="min-h-screen pb-20 pt-10 px-4 max-w-6xl mx-auto">
|
|
217
|
+
{/* Header */}
|
|
218
|
+
<section className="text-center mb-16 space-y-4">
|
|
219
|
+
<motion.div
|
|
220
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
221
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
222
|
+
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"
|
|
223
|
+
>
|
|
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>
|
|
228
|
+
</motion.div>
|
|
229
|
+
<h1 className={`${fontCursive.className} text-5xl md:text-7xl text-wedding-pink-600 dark:text-wedding-pink-400 py-2`}>
|
|
230
|
+
Digital Guestbook
|
|
231
|
+
</h1>
|
|
232
|
+
<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.
|
|
234
|
+
</p>
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
{/* Upload Section */}
|
|
238
|
+
<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">
|
|
240
|
+
<Input
|
|
241
|
+
isRequired
|
|
242
|
+
label="Your Name"
|
|
243
|
+
labelPlacement="outside-top"
|
|
244
|
+
placeholder="Who's behind the camera?"
|
|
245
|
+
variant="bordered"
|
|
246
|
+
value={formData.name}
|
|
247
|
+
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
|
+
/>
|
|
254
|
+
|
|
255
|
+
<div className="relative">
|
|
256
|
+
<input
|
|
257
|
+
type="file"
|
|
258
|
+
accept="image/*"
|
|
259
|
+
multiple={false}
|
|
260
|
+
onChange={handleFileChange}
|
|
261
|
+
className="hidden"
|
|
262
|
+
id="photo-upload"
|
|
263
|
+
/>
|
|
264
|
+
<label
|
|
265
|
+
htmlFor="photo-upload"
|
|
266
|
+
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"
|
|
267
|
+
>
|
|
268
|
+
{formData.file ? (
|
|
269
|
+
<div className="text-center text-wedding-pink-600 font-bold">
|
|
270
|
+
✓ {formData.file.name}
|
|
271
|
+
</div>
|
|
272
|
+
) : (
|
|
273
|
+
<>
|
|
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>
|
|
278
|
+
</svg>
|
|
279
|
+
<span className="text-xs text-default-500 font-medium">Select a Photo to Upload</span>
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</label>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<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
|
|
288
|
+
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
|
+
classNames={{
|
|
296
|
+
label: "text-default-600 text-xs",
|
|
297
|
+
input: "text-center font-bold text-xl outline-none",
|
|
298
|
+
}}
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<Button
|
|
303
|
+
type="submit"
|
|
304
|
+
isLoading={uploading}
|
|
305
|
+
isDisabled={!formData.file || !formData.name}
|
|
306
|
+
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"
|
|
307
|
+
>
|
|
308
|
+
Share Memory
|
|
309
|
+
</Button>
|
|
310
|
+
</form>
|
|
311
|
+
</Card>
|
|
312
|
+
|
|
313
|
+
<Divider className="mb-16 opacity-50" />
|
|
314
|
+
|
|
315
|
+
{/* Photos Grid */}
|
|
316
|
+
<div className="columns-1 sm:columns-2 lg:columns-3 gap-6 space-y-6">
|
|
317
|
+
<AnimatePresence>
|
|
318
|
+
{loading ? (
|
|
319
|
+
<div className="col-span-full flex justify-center py-20">
|
|
320
|
+
<Spinner color="danger" />
|
|
321
|
+
</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
|
+
}}
|
|
338
|
+
>
|
|
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>
|
|
365
|
+
|
|
366
|
+
{/* Admin Delete Button */}
|
|
367
|
+
{user?.email === "titas@titas.titas" && (
|
|
368
|
+
<Button
|
|
369
|
+
isIconOnly
|
|
370
|
+
size="sm"
|
|
371
|
+
variant="light"
|
|
372
|
+
color="danger"
|
|
373
|
+
className="min-w-0 p-0 h-auto delete-btn"
|
|
374
|
+
onClick={() => handleDeleteClick(photo.id)}
|
|
375
|
+
>
|
|
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>
|
|
379
|
+
</svg>
|
|
380
|
+
</Button>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</CardBody>
|
|
384
|
+
</Card>
|
|
385
|
+
</motion.div>
|
|
386
|
+
))}
|
|
387
|
+
</AnimatePresence>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Lightbox Modal */}
|
|
391
|
+
<Modal
|
|
392
|
+
isOpen={isOpen}
|
|
393
|
+
onOpenChange={onOpenChange}
|
|
394
|
+
backdrop="blur"
|
|
395
|
+
size="full"
|
|
396
|
+
classNames={{
|
|
397
|
+
wrapper: "z-[200]",
|
|
398
|
+
backdrop: "bg-black/80 backdrop-blur-md",
|
|
399
|
+
base: "bg-transparent shadow-none border-none",
|
|
400
|
+
closeButton: "hover:bg-white/20 active:bg-white/10 text-white text-2xl z-50",
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
<ModalContent>
|
|
404
|
+
{(onClose) => (
|
|
405
|
+
<ModalBody
|
|
406
|
+
className="p-4 flex items-center justify-center h-screen w-screen overflow-hidden cursor-pointer"
|
|
407
|
+
onClick={onClose}
|
|
408
|
+
>
|
|
409
|
+
{selectedImage && (
|
|
410
|
+
<motion.div
|
|
411
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
412
|
+
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()}
|
|
415
|
+
>
|
|
416
|
+
<Image
|
|
417
|
+
alt={`Memory by ${selectedImage.name}`}
|
|
418
|
+
src={selectedImage.imageUrl}
|
|
419
|
+
className="max-h-[80vh] w-auto object-contain rounded-2xl shadow-2xl"
|
|
420
|
+
/>
|
|
421
|
+
<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>
|
|
425
|
+
</div>
|
|
426
|
+
</motion.div>
|
|
427
|
+
)}
|
|
428
|
+
</ModalBody>
|
|
429
|
+
)}
|
|
430
|
+
</ModalContent>
|
|
431
|
+
</Modal>
|
|
432
|
+
|
|
433
|
+
{/* Delete Confirmation Modal */}
|
|
434
|
+
<Modal
|
|
435
|
+
isOpen={isConfirmOpen}
|
|
436
|
+
onOpenChange={setIsConfirmOpen}
|
|
437
|
+
backdrop="blur"
|
|
438
|
+
size="xs"
|
|
439
|
+
className="dark:bg-zinc-900 border border-default-100 dark:border-zinc-800"
|
|
440
|
+
>
|
|
441
|
+
<ModalContent>
|
|
442
|
+
{(onClose) => (
|
|
443
|
+
<>
|
|
444
|
+
<ModalBody className="pt-8 pb-4 text-center space-y-4">
|
|
445
|
+
<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>
|
|
450
|
+
</div>
|
|
451
|
+
<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>
|
|
454
|
+
</div>
|
|
455
|
+
</ModalBody>
|
|
456
|
+
<ModalFooter className="flex-col gap-2 pb-8">
|
|
457
|
+
<Button color="danger" className="w-full font-bold h-12" onPress={handleConfirmDelete}>
|
|
458
|
+
Yes, Delete Forever
|
|
459
|
+
</Button>
|
|
460
|
+
<Button variant="flat" className="w-full font-semibold h-12" onPress={onClose}>
|
|
461
|
+
Cancel
|
|
462
|
+
</Button>
|
|
463
|
+
</ModalFooter>
|
|
464
|
+
</>
|
|
465
|
+
)}
|
|
466
|
+
</ModalContent>
|
|
467
|
+
</Modal>
|
|
468
|
+
</div>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "Wedding Invitation | Titas & Sukanya",
|
|
5
|
+
description:
|
|
6
|
+
"We cordially invite you to celebrate the wedding of Titas and Sukanya. Click to view your personalized invitation and event details.",
|
|
7
|
+
openGraph: {
|
|
8
|
+
title: "You're Invited! | Titas & Sukanya Wedding",
|
|
9
|
+
description:
|
|
10
|
+
"We would be honored to have you join us as we celebrate our wedding. View details for the ceremony and reception.",
|
|
11
|
+
url: "https://www.titas-sukanya-for.life/invitation",
|
|
12
|
+
type: "website",
|
|
13
|
+
images: [
|
|
14
|
+
{
|
|
15
|
+
url: "/invite.png", // Updated to use invite.png from public root
|
|
16
|
+
width: 1200,
|
|
17
|
+
height: 630,
|
|
18
|
+
alt: "Titas & Sukanya Wedding Invitation",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
twitter: {
|
|
23
|
+
card: "summary_large_image",
|
|
24
|
+
title: "Wedding Invitation | Titas & Sukanya",
|
|
25
|
+
description: "Join us in celebrating our special day.",
|
|
26
|
+
images: ["/invite.png"], // Updated to use invite.png from public root
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default function InvitationLayout({
|
|
31
|
+
children,
|
|
32
|
+
}: {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}) {
|
|
35
|
+
return <div className="w-full">{children}</div>;
|
|
36
|
+
}
|