diff --git a/frontend/src/app/(public)/invite/[slug]/gifts/page.tsx b/frontend/src/app/(public)/invite/[slug]/gifts/page.tsx new file mode 100644 index 0000000..34ed71a --- /dev/null +++ b/frontend/src/app/(public)/invite/[slug]/gifts/page.tsx @@ -0,0 +1,512 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useParams, useSearchParams } from "next/navigation"; +import { useGifts } from "@/context/gift-context"; +import { useGuests } from "@/context/guest-context"; +import { GiftItem, GiftPurchase } from "@/client/types.gen"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { AlertCircle, ExternalLink, Gift, Loader2 } from "lucide-react"; + +export default function GiftRegistryPage() { + const { slug } = useParams<{ slug: string }>(); + + const searchParams = useSearchParams(); + const invitationCode = searchParams.get("code"); + + // States + const [activeTab, setActiveTab] = useState("available"); + const [isReserving, setIsReserving] = useState(false); + const [selectedGift, setSelectedGift] = useState(null); + const [quantity, setQuantity] = useState("1"); + const [errorMessage, setErrorMessage] = useState(null); + const [availableGifts, setAvailableGifts] = useState([]); + const [reservedGifts, setReservedGifts] = useState([]); + const [guestPurchases, setGuestPurchases] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Context hooks + const { + items, + isLoadingItems, + refetchItems, + reserveItem, + cancelReservation, + setCurrentEventId, + fetchPurchasesByGuest, + } = useGifts(); + + const { findGuestByInvitationCode, guests, isLoadingGuests } = useGuests(); + + // Find current guest based on invitation code + const currentGuest = invitationCode + ? findGuestByInvitationCode(invitationCode) + : undefined; + + // Initialize when guest data is available + useEffect(() => { + if (isLoadingGuests || !invitationCode) return; + + if (!currentGuest) { + setErrorMessage( + "Invalid invitation code. Please check your invitation link.", + ); + setIsLoading(false); + return; + } + + // Load event gifts + const eventId = currentGuest.event_id; + if (eventId) { + setCurrentEventId(eventId); + refetchItems(undefined, eventId) + .catch((error) => { + console.error("Error fetching gifts:", error); + setErrorMessage( + "Unable to load gift registry. Please try again later.", + ); + }) + .finally(() => { + setIsLoading(false); + }); + + // Fetch guest's purchases/reservations + fetchPurchasesByGuest(currentGuest.id) + .then((purchases) => { + if (purchases) { + setGuestPurchases(purchases); + } + }) + .catch((error) => { + console.error("Error fetching guest purchases:", error); + }); + } else { + setErrorMessage("No event found for this invitation."); + setIsLoading(false); + } + }, [ + currentGuest, + invitationCode, + isLoadingGuests, + refetchItems, + setCurrentEventId, + fetchPurchasesByGuest, + ]); + + // Process gifts into available and reserved categories + useEffect(() => { + if (items && guestPurchases.length >= 0) { + // Get IDs of gifts that this guest has reserved + const reservedGiftIds = new Set( + guestPurchases.map((purchase) => purchase.gift_id), + ); + + // Available gifts (visible and not fully reserved) + const available = items.filter( + (item) => + item.is_visible && + !reservedGiftIds.has(item.id) && + (item.status === "available" || + (item.quantity_requested && + item.quantity_received && + item.quantity_received < item.quantity_requested)), + ); + + // Reserved gifts (this guest has reserved them) + const reserved = items.filter((item) => reservedGiftIds.has(item.id)); + + setAvailableGifts(available); + setReservedGifts(reserved); + } + }, [items, guestPurchases]); + + // Format priority for display + const formatPriority = (priority: string) => { + switch (priority) { + case "must_have": + return { + label: "Must Have", + color: "bg-red-100 text-red-800 border-red-200", + }; + case "high": + return { + label: "High", + color: "bg-orange-100 text-orange-800 border-orange-200", + }; + case "medium": + return { + label: "Medium", + color: "bg-blue-100 text-blue-800 border-blue-200", + }; + case "low": + return { + label: "Low", + color: "bg-green-100 text-green-800 border-green-200", + }; + default: + return { + label: priority, + color: "bg-gray-100 text-gray-800 border-gray-200", + }; + } + }; + + // Reserve gift handler + const handleReserveClick = (gift: GiftItem) => { + setSelectedGift(gift); + setQuantity("1"); + setIsReserving(true); + }; + + // Remove reservation handler + const handleRemoveReservation = async (gift: GiftItem) => { + if (!currentGuest) { + setErrorMessage("Guest information not found."); + return; + } + + try { + await cancelReservation(gift.id, currentGuest.id); + + // Refresh data + if (currentGuest.event_id) { + await refetchItems(undefined, currentGuest.event_id); + const updatedPurchases = await fetchPurchasesByGuest(currentGuest.id); + if (updatedPurchases) { + setGuestPurchases(updatedPurchases); + } + } + } catch (error) { + console.error("Error removing reservation:", error); + setErrorMessage("Unable to remove reservation. Please try again."); + } + }; + + // Confirm reservation handler + const handleConfirmReservation = async () => { + console.debug("Confirm reservation:", currentGuest); + if (!selectedGift || !currentGuest) { + console.error("Missing information ", { selectedGift, currentGuest }); + setErrorMessage("Required information missing."); + return; + } + console.log("Reserving: ", { selectedGift, currentGuest }); + + try { + await reserveItem( + selectedGift.id, + currentGuest.id, + parseInt(quantity, 10), + ); + setIsReserving(false); + + // Refresh data + if (currentGuest.event_id) { + await refetchItems(undefined, currentGuest.event_id); + const updatedPurchases = await fetchPurchasesByGuest(currentGuest.id); + if (updatedPurchases) { + setGuestPurchases(updatedPurchases); + } + } + } catch (error) { + console.error("Error reserving gift:", error); + setErrorMessage("Unable to reserve gift. Please try again."); + } + }; + + // Get reservation quantity for a gift + const getReservationQuantity = (gift: GiftItem): number => { + const purchase = guestPurchases.find((p) => p.gift_id === gift.id); + return purchase?.quantity || 0; + }; + + // Generate quantity options for the dialog + const getQuantityOptions = (gift: GiftItem) => { + if (!gift.quantity_requested) return [1]; // Default to 1 if no quantity specified + + const remaining = gift.quantity_requested - (gift.quantity_received || 0); + return Array.from( + { length: remaining > 0 ? remaining : 1 }, + (_, i) => i + 1, + ); + }; + + // Loading state + if (isLoading || isLoadingGuests || isLoadingItems) { + return ( +
+ +

Loading gift registry...

+
+ ); + } + + // Error state + if (errorMessage) { + return ( +
+ + + Error + {errorMessage} + +
+ ); + } + + return ( +
+
+

+ Gift Registry 🎁 +

+

+ Choose a gift from the wishlist to help celebrate Emma's 1st birthday +

+
+ + + + + Available Gifts + + + Your Reservations{" "} + {reservedGifts.length > 0 && `(${reservedGifts.length})`} + + + + + {availableGifts.length === 0 ? ( + + No Available Gifts + + All gifts have been reserved or there are no gifts available at + this time. + + + ) : ( +
+ + + + Name + Priority + Link + Description + + + + + {availableGifts.map((gift) => { + const priority = formatPriority(gift.priority || "medium"); + return ( + + + {gift.name} + + + + {priority.label} + + + + {gift.purchase_url ? ( + + + View Store + + ) : ( + + None + + )} + + {gift.description || ""} + + + + + ); + })} + +
+
+ )} +
+ + + {reservedGifts.length === 0 ? ( + + No Reservations + + You haven't reserved any gifts yet. Switch to the "Available + Gifts" tab to make a selection. + + + ) : ( +
+ + + + Name + Priority + Link + Quantity + Description + + + + + {reservedGifts.map((gift) => { + const priority = formatPriority(gift.priority || "medium"); + const quantity = getReservationQuantity(gift); + return ( + + + {gift.name} + + + + {priority.label} + + + + {gift.purchase_url ? ( + + + View Store + + ) : ( + + None + + )} + + {quantity} + {gift.description || ""} + + + + + ); + })} + +
+
+ )} +
+
+ + {/* Reservation Dialog */} + + + + Reserve Gift + + {selectedGift?.name} - {selectedGift?.description} + + + +
+
+ + +
+ + {selectedGift?.purchase_url && ( +
+

Where to buy:

+ + + Visit Store + +
+ )} +
+ + + + + +
+
+
+ ); +} diff --git a/frontend/src/app/(public)/invite/[slug]/page.tsx b/frontend/src/app/(public)/invite/[slug]/page.tsx index b9d04d4..8c474e5 100644 --- a/frontend/src/app/(public)/invite/[slug]/page.tsx +++ b/frontend/src/app/(public)/invite/[slug]/page.tsx @@ -8,8 +8,9 @@ import { Loader2 } from "lucide-react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { RSVPModal } from "@/components/rsvp/rsvp-modal"; -import { useParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { getServerFileUrl } from "@/lib/utils"; +import Link from "next/link"; // Helper function to get server file URL (assuming it exists in your codebase) // If not, replace with your actual implementation @@ -20,6 +21,7 @@ const InvitationPage = () => { const [showRSVP, setShowRSVP] = useState(false); const { themes, isLoadingThemes } = useEventThemes(); const guestId = "current-guest-id-placeholder"; + const searchParams = useSearchParams(); // Fetch event data when slug is available useEffect(() => { @@ -160,6 +162,8 @@ const InvitationPage = () => { const foregroundImageUrl = getServerFileUrl(eventTheme?.foreground_image_url); const backgroundImageUrl = getServerFileUrl(eventTheme?.background_image_url); const threeAnimalsImgUrl = getAssetUrl("three-animals"); + const invitationCode = searchParams.get("code"); + const giftRegistryPageUrl = `/invite/${slug}/gifts?code=${invitationCode}`; return (
{ color: "white", }} onClick={() => setShowRSVP(true)} + disabled={!invitationCode} > PRESENZA @@ -415,9 +420,9 @@ const InvitationPage = () => { backgroundColor: colors.secondary, color: "white", }} - onClick={() => window.open(`/gifts/${slug}`, "_blank")} + disabled={!invitationCode} > - LISTA REGALI + LISTA REGALI
)}