"use client"; import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Card, CardBody, Button, Input, Image, addToast, Spinner, Divider, Avatar, Modal, ModalContent, ModalBody, ModalFooter, useDisclosure, } from "@heroui/react"; import { getFirestore, collection, addDoc, onSnapshot, query, orderBy, serverTimestamp, deleteDoc, doc, vector, getDocs, where, } from "firebase/firestore"; import { getAuth, onAuthStateChanged } from "firebase/auth"; import firebaseApp from "@/config/firebase"; import { fontCursive, fontSans } from "@/config/fonts"; import FaceRecognition from "@/app/guestbook/components/FaceRecognition"; const db = getFirestore(firebaseApp()); const auth = getAuth(firebaseApp()); // --- CLOUDINARY CONFIGURATION --- const CLOUDINARY_CLOUD_NAME = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME; const CLOUDINARY_UPLOAD_PRESET = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET; const CLOUDINARY_URL = `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`; // --------------------------------- const timeAgo = (timestamp: any) => { if (!timestamp?.toDate) return "Just now"; const now = new Date(); const diff = (now.getTime() - timestamp.toDate().getTime()) / 1000; if (diff < 60) return `${Math.floor(diff)}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; }; export default function GuestbookPage() { const [photos, setPhotos] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [uploadStatus, setUploadStatus] = useState(""); const [user, setUser] = useState(null); const [formData, setFormData] = useState({ name: "", file: null as File | null, captcha: "", }); const [faceData, setFaceData] = useState([]); const [isDetecting, setIsDetecting] = useState(false); const { isOpen, onOpen, onOpenChange } = useDisclosure(); const [selectedImage, setSelectedImage] = useState(null); // Delete Confirmation States const [imageToDelete, setImageToDelete] = useState(null); const [isConfirmOpen, setIsConfirmOpen] = useState(false); useEffect(() => { const unsubscribeAuth = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); }); return () => unsubscribeAuth(); }, []); const handleDeleteClick = (id: string) => { setImageToDelete(id); setIsConfirmOpen(true); }; const handleConfirmDelete = async () => { if (!imageToDelete) return; // Find the photo object to get the URL const photo = photos.find((p) => p.id === imageToDelete); if (!photo) return; try { // 1. Delete from Guestbook Collection await deleteDoc(doc(db, "guestbook", imageToDelete)); // 2. Delete from Facedata Collection (all faces associated with this photo) const q = query(collection(db, "facedata"), where("imageUrl", "==", photo.imageUrl)); const querySnapshot = await getDocs(q); const deletePromises = querySnapshot.docs.map((doc) => deleteDoc(doc.ref)); await Promise.all(deletePromises); addToast({ title: "Deleted", description: "Memory and associated face data removed.", color: "success", }); } catch (err) { console.error("Delete error:", err); addToast({ title: "Error", description: "Failed to delete completely.", color: "danger", }); } finally { setIsConfirmOpen(false); setImageToDelete(null); } }; const resizeImage = (file: File): Promise => { return new Promise((resolve) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (event) => { const img = new window.Image(); img.src = event.target?.result as string; img.onload = () => { const canvas = document.createElement("canvas"); const MAX_WIDTH = 1200; const MAX_HEIGHT = 1200; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx?.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { if (blob) resolve(blob); }, "image/jpeg", 0.7, // Compression quality ); }; }; }); }; const openLightbox = (image: any) => { setSelectedImage(image); onOpen(); }; useEffect(() => { const q = query(collection(db, "guestbook"), orderBy("createdAt", "desc")); const unsubscribe = onSnapshot(q, (snapshot) => { const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })); setPhotos(data); setLoading(false); }); return () => unsubscribe(); }, []); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setFormData({ ...formData, file: e.target.files[0] }); } }; const handleUpload = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.file || !formData.name) return; // Bot Protection if (formData.captcha.trim() !== "10") { addToast({ title: "Security Check", description: "Please answer the question correctly (Hint: 10 years!).", color: "warning", }); return; } setUploading(true); try { // 1. Resize and compress image setUploadStatus("Resizing Photo..."); const resizedBlob = await resizeImage(formData.file); // 2. Upload to Cloudinary setUploadStatus("Uploading to Cloud..."); const data = new FormData(); data.append("file", resizedBlob, "upload.jpg"); data.append("upload_preset", CLOUDINARY_UPLOAD_PRESET!); const res = await fetch(CLOUDINARY_URL, { method: "POST", body: data, }); const file = await res.json(); if (file.secure_url) { // 2. Save URL to Firestore setUploadStatus("Saving Memory..."); await addDoc(collection(db, "guestbook"), { name: formData.name, imageUrl: file.secure_url, createdAt: serverTimestamp(), }); // 3. Save Face Data to Firestore (One document per face for Vector Search) if (faceData.length > 0) { setUploadStatus("Finalizing Face Data..."); const facePromises = faceData.map((face) => { return addDoc(collection(db, "facedata"), { photoId: file.secure_url, // Using URL as ID linkage for simplicity, or we could use the doc ref id from above if we awaited it imageUrl: file.secure_url, name: formData.name, createdAt: serverTimestamp(), embedding: vector(face.descriptor), // Top-level vector field descriptor: face.descriptor, // Backup raw array metadata: { detectionScore: face.detection.score, // other metadata if needed }, }); }); await Promise.all(facePromises); } addToast({ title: "Memory Shared!", description: "Your photo has been added to our guestbook.", color: "success", }); setFormData({ name: "", file: null, captcha: "" }); setFaceData([]); } } catch (err) { console.error("Upload error", err); addToast({ title: "Upload Failed", description: "Something went wrong. Please try again.", color: "danger", }); } finally { setUploading(false); setUploadStatus(""); } }; return (
{/* Header */}

Digital Guestbook

Capture a moment and share it with us! Upload a selfie or a memory to be part of our wedding gallery forever.

{user?.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL && ( )}
{/* Upload Section */}
setFormData({ ...formData, name: e.target.value })} />
{formData.file ? (
Upload Preview

{formData.file.name}

) : (
{/* Take Selfie - Mobile Only */} {/* From Gallery - Desktop version (Big Button) */} {/* From Gallery - Mobile version (Text link) */}
)}

Bot Protection

setFormData({ ...formData, captcha: e.target.value }) } />
{!isDetecting && formData.file && faceData.length === 0 && (

⚠️ No face detected. This photo won't be found via selfie search.

)}
{/* Photos Grid */}
{loading ? (
) : ( photos.map((photo, i) => ( { // Only open lightbox if NOT clicking the delete button if (!(e.target as HTMLElement).closest(".delete-btn")) { openLightbox(photo); } }} > {`Memory {/* Expand Icon Indicator */}
{/* Info Overlay */}

{photo.name}

{timeAgo(photo.createdAt)}

{/* Admin Delete Button */} {user?.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL && ( )}
)) )}
{/* Lightbox Modal */} {(onClose) => ( {selectedImage && ( {`Memory

Memory by {selectedImage.name}

)}
)}
{/* Delete Confirmation Modal */} {(onClose) => ( <>

Delete Memory?

This action cannot be undone. This memory will be gone forever.

)}
); }