From 44d6f6d8377a4e8fc83c860e3900968d05b14245 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sun, 16 Mar 2025 16:05:14 +0100 Subject: [PATCH] Add initial implementation of gift context in frontend --- frontend/src/context/gift-context.tsx | 780 ++++++++++++++++++++++++++ frontend/src/providers/data.tsx | 5 +- 2 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 frontend/src/context/gift-context.tsx diff --git a/frontend/src/context/gift-context.tsx b/frontend/src/context/gift-context.tsx new file mode 100644 index 0000000..0b51d38 --- /dev/null +++ b/frontend/src/context/gift-context.tsx @@ -0,0 +1,780 @@ +"use client"; + +import React, { createContext, ReactNode, useContext } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + createGiftCategory, + readGiftCategories, + deleteGiftCategory, + readGiftCategory, + updateGiftCategory, + associateCategoryWithEvent, + updateCategoryEventSettings, + getEventsForCategory, + reorderGiftsInCategory, + createGiftItem, + readGiftItems, + deleteGiftItem, + readGiftItem, + updateGiftItem, + updateGiftItemStatus, + reserveGiftItem, + cancelGiftReservation, + createGiftPurchase, + readGiftPurchase, + readGiftPurchasesByGift, + readGiftPurchasesByGuest, +} from "@/client/sdk.gen"; +import { + GiftCategory, + GiftCategoryCreate, + GiftCategoryUpdate, + GiftItem, + GiftItemCreate, + GiftItemUpdate, + GiftStatus, + GiftPriority, + GiftPurchase, + GiftPurchaseCreate, +} from "@/client/types.gen"; + +// Gift context state +interface GiftContextState { + // Gift Categories + categories: GiftCategory[] | undefined; + category: GiftCategory | undefined; + isLoadingCategories: boolean; + isLoadingCategory: boolean; + refetchCategories: (eventId: string) => Promise; + + fetchCategoryById: (id: string, eventId?: string) => void; + createCategory: (data: GiftCategoryCreate) => Promise; + updateCategory: ( + id: string, + data: GiftCategoryUpdate, + eventId?: string + ) => Promise; + deleteCategory: (id: string, eventId?: string) => Promise; + associateCategoryWithEvent: ( + categoryId: string, + eventId: string, + displayOrder?: number, + isVisible?: boolean + ) => Promise; + updateCategoryEventSettings: ( + categoryId: string, + eventId: string, + displayOrder?: number, + isVisible?: boolean + ) => Promise; + getEventsForCategory: (categoryId: string) => Promise; + reorderGiftsInCategory: ( + categoryId: string, + giftIds: string[] + ) => Promise; + + // Gift Items + items: GiftItem[] | undefined; + item: GiftItem | undefined; + isLoadingItems: boolean; + isLoadingItem: boolean; + refetchItems: (categoryId?: string, eventId?: string) => Promise; + + fetchItemById: (id: string) => void; + createItem: (data: GiftItemCreate) => Promise; + updateItem: ( + id: string, + data: GiftItemUpdate + ) => Promise; + deleteItem: (id: string) => Promise; + updateItemStatus: ( + id: string, + status: GiftStatus + ) => Promise; + reserveItem: ( + id: string, + guestId: string, + quantity?: number + ) => Promise; + cancelReservation: ( + id: string, + guestId: string + ) => Promise; + + // Gift Purchases + purchases: GiftPurchase[] | undefined; + purchase: GiftPurchase | undefined; + isLoadingPurchases: boolean; + isLoadingPurchase: boolean; + refetchPurchases: (giftId?: string, guestId?: string) => Promise; + + fetchPurchaseById: (id: string) => void; + createPurchase: (data: GiftPurchaseCreate) => Promise; + fetchPurchasesByGift: (giftId: string) => Promise; + fetchPurchasesByGuest: (guestId: string) => Promise; + + // Current selections + currentCategoryId: string | null; + setCurrentCategoryId: (id: string | null) => void; + currentItemId: string | null; + setCurrentItemId: (id: string | null) => void; + currentPurchaseId: string | null; + setCurrentPurchaseId: (id: string | null) => void; + currentEventId: string | null; + setCurrentEventId: (id: string | null) => void; + + error: Error | null; +} + +// Default context state +const defaultGiftContextState: GiftContextState = { + // Gift Categories + categories: undefined, + category: undefined, + isLoadingCategories: false, + isLoadingCategory: false, + refetchCategories: async () => { + throw new Error("GiftContext not initialized"); + }, + + fetchCategoryById: () => {}, + createCategory: async () => { + throw new Error("GiftContext not initialized"); + }, + updateCategory: async () => { + throw new Error("GiftContext not initialized"); + }, + deleteCategory: async () => { + throw new Error("GiftContext not initialized"); + }, + associateCategoryWithEvent: async () => { + throw new Error("GiftContext not initialized"); + }, + updateCategoryEventSettings: async () => { + throw new Error("GiftContext not initialized"); + }, + getEventsForCategory: async () => { + throw new Error("GiftContext not initialized"); + }, + reorderGiftsInCategory: async () => { + throw new Error("GiftContext not initialized"); + }, + + // Gift Items + items: undefined, + item: undefined, + isLoadingItems: false, + isLoadingItem: false, + refetchItems: async () => { + throw new Error("GiftContext not initialized"); + }, + + fetchItemById: () => {}, + createItem: async () => { + throw new Error("GiftContext not initialized"); + }, + updateItem: async () => { + throw new Error("GiftContext not initialized"); + }, + deleteItem: async () => { + throw new Error("GiftContext not initialized"); + }, + updateItemStatus: async () => { + throw new Error("GiftContext not initialized"); + }, + reserveItem: async () => { + throw new Error("GiftContext not initialized"); + }, + cancelReservation: async () => { + throw new Error("GiftContext not initialized"); + }, + + // Gift Purchases + purchases: undefined, + purchase: undefined, + isLoadingPurchases: false, + isLoadingPurchase: false, + refetchPurchases: async () => { + throw new Error("GiftContext not initialized"); + }, + + fetchPurchaseById: () => {}, + createPurchase: async () => { + throw new Error("GiftContext not initialized"); + }, + fetchPurchasesByGift: async () => { + throw new Error("GiftContext not initialized"); + }, + fetchPurchasesByGuest: async () => { + throw new Error("GiftContext not initialized"); + }, + + // Current selections + currentCategoryId: null, + setCurrentCategoryId: () => {}, + currentItemId: null, + setCurrentItemId: () => {}, + currentPurchaseId: null, + setCurrentPurchaseId: () => {}, + currentEventId: null, + setCurrentEventId: () => {}, + + error: null, +}; + +// Create context +const GiftContext = createContext(defaultGiftContextState); + +// Hook to use context +export const useGifts = () => { + const context = useContext(GiftContext); + if (!context) { + throw new Error("useGifts must be used within a GiftProvider"); + } + return context; +}; + +// Gift Provider Props +interface GiftProviderProps { + children: ReactNode; +} + +// Gift Provider Component +export const GiftProvider: React.FC = ({ children }) => { + const queryClient = useQueryClient(); + const [currentCategoryId, setCurrentCategoryId] = React.useState(null); + const [currentItemId, setCurrentItemId] = React.useState(null); + const [currentPurchaseId, setCurrentPurchaseId] = React.useState(null); + const [currentEventId, setCurrentEventId] = React.useState(null); + + // Fetch all categories for an event + const { + data: categories, + isLoading: isLoadingCategories, + error: categoriesError, + refetch: refetchCategoriesInternal, + } = useQuery({ + queryKey: ["giftCategories", currentEventId], + queryFn: () => + currentEventId + ? readGiftCategories({ + path: { event_id: currentEventId } + }).then((res) => res.data) + : Promise.resolve(undefined), + enabled: !!currentEventId, + }); + + const refetchCategories = async (eventId: string) => { + setCurrentEventId(eventId); + return refetchCategoriesInternal(); + }; + + // Fetch specific category + const { + data: category, + isLoading: isLoadingCategory, + error: categoryError, + } = useQuery({ + queryKey: ["giftCategory", currentCategoryId, currentEventId], + queryFn: () => + currentCategoryId + ? readGiftCategory({ + path: { + category_id: currentCategoryId + }, + query: currentEventId ? { event_id: currentEventId } : undefined + }).then((res) => res.data) + : Promise.resolve(undefined), + enabled: !!currentCategoryId, + }); + + const fetchCategoryById = (id: string, eventId?: string) => { + setCurrentCategoryId(id); + if (eventId) { + setCurrentEventId(eventId); + } + }; + + // Create Category Mutation + const createCategoryMutation = useMutation({ + mutationFn: (data: GiftCategoryCreate) => + createGiftCategory({ body: data }).then((res) => res.data), + onSuccess: () => { + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + }, + }); + + // Update Category Mutation + const updateCategoryMutation = useMutation({ + mutationFn: ({ + id, + data, + eventId + }: { + id: string; + data: GiftCategoryUpdate; + eventId?: string; + }) => + updateGiftCategory({ + path: { category_id: id }, + body: data, + query: eventId ? { event_id: eventId } : undefined + }).then((res) => res.data), + onSuccess: () => { + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftCategory", currentCategoryId] }); + } + }, + }); + + // Delete Category Mutation + const deleteCategoryMutation = useMutation({ + mutationFn: ({ id, eventId }: { id: string; eventId?: string }) => + deleteGiftCategory({ + path: { category_id: id }, + query: eventId ? { event_id: eventId } : undefined + }).then((res) => res.data), + onSuccess: () => { + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + }, + }); + + // Fetch all items for a category or event + const { + data: items, + isLoading: isLoadingItems, + error: itemsError, + refetch: refetchItemsInternal, + } = useQuery({ + queryKey: ["giftItems", currentCategoryId, currentEventId], + queryFn: () => { + const query: Record = {}; + if (currentCategoryId) { + query.category_id = currentCategoryId; + } + if (currentEventId) { + query.event_id = currentEventId; + } + + return Object.keys(query).length > 0 + ? readGiftItems({ query }).then((res) => res.data) + : Promise.resolve(undefined); + }, + enabled: !!(currentCategoryId || currentEventId), + }); + + const refetchItems = async (categoryId?: string, eventId?: string) => { + if (categoryId) { + setCurrentCategoryId(categoryId); + } + if (eventId) { + setCurrentEventId(eventId); + } + return refetchItemsInternal(); + }; + + // Fetch specific item + const { + data: item, + isLoading: isLoadingItem, + error: itemError, + } = useQuery({ + queryKey: ["giftItem", currentItemId], + queryFn: () => + currentItemId + ? readGiftItem({ + path: { + gift_id: currentItemId + } + }).then((res) => res.data) + : Promise.resolve(undefined), + enabled: !!currentItemId, + }); + + const fetchItemById = (id: string) => { + setCurrentItemId(id); + }; + + // Create Item Mutation + const createItemMutation = useMutation({ + mutationFn: (data: GiftItemCreate) => + createGiftItem({ body: data }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + }, + }); + + // Update Item Mutation + const updateItemMutation = useMutation({ + mutationFn: ({ + id, + data + }: { + id: string; + data: GiftItemUpdate; + }) => + updateGiftItem({ + path: { gift_id: id }, + body: data + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + if (currentItemId) { + queryClient.invalidateQueries({ queryKey: ["giftItem", currentItemId] }); + } + }, + }); + + // Delete Item Mutation + const deleteItemMutation = useMutation({ + mutationFn: (id: string) => + deleteGiftItem({ + path: { gift_id: id } + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + }, + }); + + // Update Item Status Mutation + const updateItemStatusMutation = useMutation({ + mutationFn: ({ + id, + status + }: { + id: string; + status: GiftStatus; + }) => + updateGiftItemStatus({ + path: { gift_id: id }, + body: { status } + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + if (currentItemId) { + queryClient.invalidateQueries({ queryKey: ["giftItem", currentItemId] }); + } + }, + }); + + // Reserve Item Mutation + const reserveItemMutation = useMutation({ + mutationFn: ({ + id, + guestId, + quantity + }: { + id: string; + guestId: string; + quantity?: number; + }) => + reserveGiftItem({ + path: { gift_id: id }, + body: { + guest_id: guestId, + quantity + } + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + if (currentItemId) { + queryClient.invalidateQueries({ queryKey: ["giftItem", currentItemId] }); + } + }, + }); + + // Cancel Reservation Mutation + const cancelReservationMutation = useMutation({ + mutationFn: ({ + id, + guestId + }: { + id: string; + guestId: string; + }) => + cancelGiftReservation({ + path: { + gift_id: id, + guest_id: guestId + } + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftItems", null, currentEventId] }); + } + if (currentItemId) { + queryClient.invalidateQueries({ queryKey: ["giftItem", currentItemId] }); + } + }, + }); + + // Associate Category With Event Mutation + const associateCategoryWithEventMutation = useMutation({ + mutationFn: ({ + categoryId, + eventId, + displayOrder, + isVisible + }: { + categoryId: string; + eventId: string; + displayOrder?: number; + isVisible?: boolean; + }) => + associateCategoryWithEvent({ + path: { + category_id: categoryId, + event_id: eventId + }, + body: { + display_order: displayOrder, + is_visible: isVisible + } + }).then((res) => res.data), + onSuccess: () => { + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + }, + }); + + // Update Category Event Settings Mutation + const updateCategoryEventSettingsMutation = useMutation({ + mutationFn: ({ + categoryId, + eventId, + displayOrder, + isVisible + }: { + categoryId: string; + eventId: string; + displayOrder?: number; + isVisible?: boolean; + }) => + updateCategoryEventSettings({ + path: { + category_id: categoryId, + event_id: eventId + }, + body: { + display_order: displayOrder, + is_visible: isVisible + } + }).then((res) => res.data), + onSuccess: () => { + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftCategory", currentCategoryId] }); + } + }, + }); + + // Get Events For Category Query + const getEventsForCategoryQuery = async (categoryId: string) => { + return getEventsForCategory({ + path: { category_id: categoryId } + }).then((res) => res.data); + }; + + // Reorder Gifts In Category Mutation + const reorderGiftsInCategoryMutation = useMutation({ + mutationFn: ({ + categoryId, + giftIds + }: { + categoryId: string; + giftIds: string[]; + }) => + reorderGiftsInCategory({ + path: { category_id: categoryId }, + body: { gift_ids: giftIds } + }).then((res) => res.data), + onSuccess: () => { + if (currentCategoryId) { + queryClient.invalidateQueries({ queryKey: ["giftCategory", currentCategoryId] }); + } + if (currentEventId) { + queryClient.invalidateQueries({ queryKey: ["giftCategories", currentEventId] }); + } + }, + }); + + // Fetch all purchases for a gift or guest + const { + data: purchases, + isLoading: isLoadingPurchases, + error: purchasesError, + refetch: refetchPurchasesInternal, + } = useQuery({ + queryKey: ["giftPurchases", currentItemId, null], + queryFn: () => + currentItemId + ? readGiftPurchasesByGift({ + path: { gift_id: currentItemId } + }).then((res) => res.data) + : Promise.resolve(undefined), + enabled: !!currentItemId, + }); + + const refetchPurchases = async (giftId?: string, guestId?: string) => { + if (giftId) { + setCurrentItemId(giftId); + } + return refetchPurchasesInternal(); + }; + + // Fetch specific purchase + const { + data: purchase, + isLoading: isLoadingPurchase, + error: purchaseError, + } = useQuery({ + queryKey: ["giftPurchase", currentPurchaseId], + queryFn: () => + currentPurchaseId + ? readGiftPurchase({ + path: { purchase_id: currentPurchaseId } + }).then((res) => res.data) + : Promise.resolve(undefined), + enabled: !!currentPurchaseId, + }); + + const fetchPurchaseById = (id: string) => { + setCurrentPurchaseId(id); + }; + + // Create Purchase Mutation + const createPurchaseMutation = useMutation({ + mutationFn: (data: GiftPurchaseCreate) => + createGiftPurchase({ body: data }).then((res) => res.data), + onSuccess: () => { + if (currentItemId) { + queryClient.invalidateQueries({ queryKey: ["giftPurchases", currentItemId] }); + queryClient.invalidateQueries({ queryKey: ["giftItem", currentItemId] }); + } + }, + }); + + // Fetch Purchases By Gift + const fetchPurchasesByGiftQuery = async (giftId: string) => { + return readGiftPurchasesByGift({ + path: { gift_id: giftId } + }).then((res) => res.data); + }; + + // Fetch Purchases By Guest + const fetchPurchasesByGuestQuery = async (guestId: string) => { + return readGiftPurchasesByGuest({ + path: { guest_id: guestId } + }).then((res) => res.data); + }; + + const contextValue: GiftContextState = { + // Gift Categories + categories, + category, + isLoadingCategories, + isLoadingCategory, + refetchCategories, + + fetchCategoryById, + createCategory: createCategoryMutation.mutateAsync, + updateCategory: (id, data, eventId) => + updateCategoryMutation.mutateAsync({ id, data, eventId }), + deleteCategory: (id, eventId) => + deleteCategoryMutation.mutateAsync({ id, eventId }), + + // Gift Items + items, + item, + isLoadingItems, + isLoadingItem, + refetchItems, + + fetchItemById, + createItem: createItemMutation.mutateAsync, + updateItem: (id, data) => + updateItemMutation.mutateAsync({ id, data }), + deleteItem: deleteItemMutation.mutateAsync, + updateItemStatus: (id, status) => + updateItemStatusMutation.mutateAsync({ id, status }), + reserveItem: (id, guestId, quantity) => + reserveItemMutation.mutateAsync({ id, guestId, quantity }), + cancelReservation: (id, guestId) => + cancelReservationMutation.mutateAsync({ id, guestId }), + + // Gift Categories additional methods + associateCategoryWithEvent: (categoryId, eventId, displayOrder, isVisible) => + associateCategoryWithEventMutation.mutateAsync({ categoryId, eventId, displayOrder, isVisible }), + updateCategoryEventSettings: (categoryId, eventId, displayOrder, isVisible) => + updateCategoryEventSettingsMutation.mutateAsync({ categoryId, eventId, displayOrder, isVisible }), + getEventsForCategory: getEventsForCategoryQuery, + reorderGiftsInCategory: (categoryId, giftIds) => + reorderGiftsInCategoryMutation.mutateAsync({ categoryId, giftIds }), + + // Gift Purchases + purchases, + purchase, + isLoadingPurchases, + isLoadingPurchase, + refetchPurchases, + + fetchPurchaseById, + createPurchase: createPurchaseMutation.mutateAsync, + fetchPurchasesByGift: fetchPurchasesByGiftQuery, + fetchPurchasesByGuest: fetchPurchasesByGuestQuery, + + // Current selections + currentCategoryId, + setCurrentCategoryId, + currentItemId, + setCurrentItemId, + currentPurchaseId, + setCurrentPurchaseId, + currentEventId, + setCurrentEventId, + + error: (categoriesError || categoryError || itemsError || itemError || purchasesError || purchaseError) as Error | null, + }; + + return ( + {children} + ); +}; diff --git a/frontend/src/providers/data.tsx b/frontend/src/providers/data.tsx index 8118beb..8c5c6a6 100644 --- a/frontend/src/providers/data.tsx +++ b/frontend/src/providers/data.tsx @@ -3,13 +3,16 @@ import { EventsProvider } from "@/context/event-context"; import { EventThemesProvider } from "@/context/event-theme-context"; import { RSVPProvider } from "@/context/rsvp-context"; import { GuestsProvider } from "@/context/guest-context"; +import { GiftProvider } from "@/context/gift-context"; export function DataProviders({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} +