Compare commits

...

9 Commits

Author SHA1 Message Date
Felipe Cardoso
62ce98c80e Add support for fetching all guest gift reservations
All checks were successful
Build and Push Docker Images / changes (push) Successful in 4s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Successful in 1m12s
Introduce new types, query functions, and SDK methods to enable retrieving all guest gift purchases via the `/api/v1/events/gifts/purchases/guest/all` endpoint. This addition integrates with React Query and ensures type safety throughout the client.
2025-03-19 10:17:43 +01:00
Felipe Cardoso
fe3f2b0894 Add API endpoint to retrieve all guest gift reservations
Introduced a new route to fetch all guest gift reservations, ensuring API compatibility with GiftPurchase objects. Refactored CRUD functions to optimize data fetching and avoid N+1 queries. Added validation to restrict access to non-public event-related reservations.
2025-03-19 10:16:55 +01:00
Felipe Cardoso
79f08a1208 Add quantity column to GuestGifts table and update references
Introduced a `quantity` field to track the number of reserved gifts in the many-to-many GuestGifts table. Updated all related CRUD operations, models, and imports to reflect the added column. Replaced `guest_gifts` with `GuestGifts` for consistency in naming conventions.
2025-03-19 09:56:30 +01:00
Felipe Cardoso
2c73ee4d7e Add purchase link and reservation details to gift list
Enhanced the gift list by showing a clickable purchase link icon and added conditional reservation details for reserved or received items using a popover. Simplified and cleaned up related code for better readability and maintainability.
2025-03-19 09:43:58 +01:00
Felipe Cardoso
392dd6f0d2 Add columns for dietary restrictions and notes to guest list
Enhanced the guest list table by adding new columns for dietary restrictions and notes. These columns display content using interactive popovers when data is available, improving data accessibility and user experience. Updated existing table structure to accommodate these enhancements.
2025-03-19 09:07:51 +01:00
Felipe Cardoso
1f1192fb62 Trigger data refetch on mount for events and themes pages
Added `useEffect` hooks to automatically refetch guests data in the Event Detail Page and themes data in the Event Themes Page upon component mount. This ensures the displayed data is always up-to-date when the user navigates to these pages.
2025-03-19 09:02:57 +01:00
Felipe Cardoso
31c6ae3f5c Add refetch methods to event context and integrate in layout
Introduce `refetchUpcomingEvents`, `refetchPublicEvents`, and `refetchUserEvents` to the event context for improved data management. These methods are now invoked in the dashboard layout to ensure events data is refreshed upon user authentication.
2025-03-19 09:00:26 +01:00
Felipe Cardoso
678c55a1e2 Integrate RSVP context and enhance form responsiveness
Added RSVP context to manage current guest and RSVP data, ensuring dynamic updates based on state changes. Adjusted styling to improve button responsiveness and removed redundant borders for a cleaner UI. These updates enhance functionality and provide a more seamless user experience.
2025-03-19 08:40:47 +01:00
Felipe Cardoso
d61e518697 Add event_id and guest_id to RSVPSchema and RSVPSchemaCreate
event_id and guest_id fields were added to both RSVPSchema and RSVPSchemaCreate to ensure these properties are included in the API schema. This change also updates the required fields list, improving completeness and validation for RSVP-related actions.
2025-03-19 08:40:37 +01:00
16 changed files with 355 additions and 109 deletions

View File

@@ -317,6 +317,7 @@ def upgrade() -> None:
sa.Column('gift_id', sa.UUID(), nullable=False),
sa.Column('reserved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=False, server_default='1'),
sa.ForeignKeyConstraint(['gift_id'], ['gift_items.id'], ondelete="CASCADE"),
sa.ForeignKeyConstraint(['guest_id'], ['guests.id'], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('guest_id', 'gift_id')

View File

@@ -1048,3 +1048,21 @@ def read_gift_purchases_by_guest(
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_purchase_crud.get_gift_reservations_by_guest(db, guest_id=guest_id)
@router.get(
"/purchases/guest/all",
response_model=List[GiftPurchase],
operation_id="read_guests_gift_purchases"
)
def read_all_guest_gift_reservations(
*,
db: Session = Depends(get_db),
) -> Any:
"""
Retrieve all guest gift reservations.
"""
reservations = gift_purchase_crud.get_all_guest_gift_reservations(db=db)
if not reservations:
reservations = []
return reservations

View File

@@ -2,15 +2,13 @@ from datetime import datetime, timezone
from typing import List, Optional, Dict, Any, Union
from uuid import UUID
from sqlalchemy import asc, desc
from sqlalchemy.orm import Session
from sqlalchemy import asc, desc, select
from sqlalchemy.orm import Session, joinedload
from app.crud.base import CRUDBase
from app.models import gift as gift_models
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus, EventGiftCategory
from app.models.guest import Guest
from app.models.event import Event
from app.schemas import gifts as gift_schemas
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus, EventGiftCategory
from app.models.guest import Guest, GuestGifts
from app.schemas.gifts import (
GiftItemCreate, GiftItemUpdate,
GiftCategoryCreate, GiftCategoryUpdate,
@@ -94,13 +92,14 @@ class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
gift: GiftItem = self.get(db, gift_id)
if gift and gift.quantity_received < gift.quantity_requested:
# Add to the association table using the SQLAlchemy Core Table directly
from app.models.guest import guest_gifts
from app.models.guest import GuestGifts
stmt = guest_gifts.insert().values(
stmt = GuestGifts.insert().values(
gift_id=gift_id,
guest_id=guest_id,
reserved_at=datetime.now(timezone.utc),
notes=notes
notes=notes,
quantity=quantity
)
db.execute(stmt)
@@ -151,6 +150,7 @@ class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
return gift # Always return the gift object, even if no changes were made
class CRUDEventGiftCategory:
def create(self, db: Session, *, obj_in: EventGiftCategoryCreate) -> EventGiftCategory:
"""Create a new event-category association"""
@@ -335,13 +335,10 @@ class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUp
result = []
for gift in guest.gifts:
# Access the association data through the relationship metadata
from sqlalchemy import select
from app.models.guest import guest_gifts
# Get the reservation data
stmt = select(guest_gifts).where(
(guest_gifts.c.guest_id == guest_id) &
(guest_gifts.c.gift_id == gift.id)
stmt = select(GuestGifts).where(
(GuestGifts.c.guest_id == guest_id) &
(GuestGifts.c.gift_id == gift.id)
)
reservation = db.execute(stmt).first()
@@ -359,6 +356,37 @@ class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUp
return result
def get_all_guest_gift_reservations(self, db: Session) -> List[GiftPurchase]:
"""Retrieve all guest gift reservations and convert them into GiftPurchase-like objects for API compatibility."""
stmt = select(Guest).options(joinedload(Guest.gifts))
guests = db.scalars(stmt).all()
result = []
for guest in guests:
# Fetch all gifts and their reservation details per guest at once to avoid N+1 issues
for gift in guest.gifts:
reservation_stmt = select(GuestGifts).where(
(GuestGifts.c.guest_id == guest.id) &
(GuestGifts.c.gift_id == gift.id)
)
reservation = db.execute(reservation_stmt).first()
if reservation:
purchase = GiftPurchase(
id=UUID('00000000-0000-0000-0000-000000000000'), # Placeholder UUID
gift_id=gift.id,
guest_id=guest.id,
quantity=1, # Default
purchased_at=reservation.reserved_at,
notes=reservation.notes
)
result.append(purchase)
return result
# Create CRUD instances
gift_item_crud = CRUDGiftItem(GiftItem)

View File

@@ -16,7 +16,7 @@ from .event_theme import EventTheme
from .event_media import EventMedia, MediaType, MediaPurpose
# Import guest and RSVP models
from .guest import Guest, GuestStatus, guest_gifts
from .guest import Guest, GuestStatus, GuestGifts
from .rsvp import RSVP, RSVPStatus
# Import gift-related models

View File

@@ -82,11 +82,12 @@ class Guest(Base, UUIDMixin, TimestampMixin):
# Association table for guest gifts (many-to-many relationship)
guest_gifts = Table(
GuestGifts = Table(
'guest_gifts',
Base.metadata,
Column('guest_id', UUID(as_uuid=True), ForeignKey('guests.id'), primary_key=True),
Column('gift_id', UUID(as_uuid=True), ForeignKey('gift_items.id'), primary_key=True),
Column('reserved_at', DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)),
Column('notes', String),
Column('quantity', Integer, default=1),
)

View File

@@ -17,6 +17,7 @@ import Link from "next/link";
import { Loader2Icon, PaletteIcon } from "lucide-react";
import { useEventThemes } from "@/context/event-theme-context";
import { getServerFileUrl } from "@/lib/utils";
import { useEffect } from "react";
export default function EventThemesPage() {
// const { data: themes, isLoading } = useQuery({
@@ -24,8 +25,11 @@ export default function EventThemesPage() {
// queryFn: () => listEventThemes().then(res => res.data),
// });
const { themes, isLoadingThemes } = useEventThemes();
const { themes, refetchThemes, isLoadingThemes } = useEventThemes();
useEffect(() => {
refetchThemes();
}, []);
return (
<div className="max-w-7xl mx-auto py-6 px-4">
<div className="flex items-center justify-between mb-8">

View File

@@ -6,16 +6,15 @@ import React, { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import {
ChevronRight,
Loader2,
AlertTriangle,
ChevronRight,
Edit,
ExternalLink,
Loader2,
MoreHorizontal,
Plus,
Search,
Filter,
Edit,
Trash,
MoreHorizontal,
Settings,
} from "lucide-react";
import { useEvents } from "@/context/event-context";
import { useGifts } from "@/context/gift-context";
@@ -45,9 +44,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { GiftStatus, GiftPriority } from "@/client/types.gen";
import { GiftPriority, GiftStatus } from "@/client/types.gen";
import { CategoryModal } from "@/components/gifts/category-modal";
import { GiftModal } from "@/components/gifts/gift-modal";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export default function GiftRegistryPage() {
const { slug } = useParams<{ slug: string }>();
@@ -57,6 +61,7 @@ export default function GiftRegistryPage() {
items,
isLoadingCategories,
isLoadingItems,
purchases,
error,
refetchCategories,
refetchItems,
@@ -387,19 +392,70 @@ export default function GiftRegistryPage() {
const category = categories?.find(
(c) => c.id === item.category_id,
);
const canShowReservations = item.status
? (
[
GiftStatus.RESERVED,
GiftStatus.RECEIVED,
] as GiftStatus[]
).includes(item.status)
: false;
return (
<React.Fragment key={item.id}>
<TableRow>
<TableCell>
{category?.name || "Uncategorized"}
</TableCell>
<TableCell className="font-medium">
<TableCell className="flex items-center gap-2 font-medium">
{/* Purchase URL clickable icon */}
{item.purchase_url ? (
<Link
href={item.purchase_url}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="w-4 h-4 text-blue-500 hover:text-blue-600" />
</Link>
) : (
<ExternalLink className="w-4 h-4 text-gray-300 cursor-not-allowed" />
)}
{item.name}
</TableCell>
<TableCell>{item.quantity_requested || 1}</TableCell>
<TableCell>{item.formatted_price || "-"}</TableCell>
<TableCell>{getPriorityBadge(item.priority)}</TableCell>
<TableCell>{getStatusBadge(item.status)}</TableCell>
<TableCell>
{canShowReservations ? (
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer">
{getStatusBadge(item.status)}
</div>
</PopoverTrigger>
{/*<PopoverContent className="text-sm">*/}
{/* {item.reservations &&*/}
{/* item.reservations.length > 0 ? (*/}
{/* <ul className="list-disc">*/}
{/* {item.reservations.map((res) => (*/}
{/* <li key={res.guest_id}>*/}
{/* {res.guest_name}: {res.quantity}*/}
{/* </li>*/}
{/* ))}*/}
{/* </ul>*/}
{/* ) : (*/}
{/* <p>No reservations available.</p>*/}
{/* )}*/}
{/*</PopoverContent>*/}
</Popover>
) : (
getStatusBadge(item.status)
)}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -428,68 +484,18 @@ export default function GiftRegistryPage() {
</DropdownMenu>
</TableCell>
</TableRow>
{/* Separate row for description */}
{/*{item.description && (*/}
<TableRow>
<TableCell
colSpan={7}
className="py-1 px-4 border-t-0 bg-gray-50 dark:bg-gray-800/50 text-sm text-gray-500 dark:text-gray-400"
className="py-1 px-4 bg-gray-50 dark:bg-gray-800/50 text-sm text-gray-500 dark:text-gray-400 h-8"
>
<div className="flex flex-col gap-1">
{/* Description - show if available */}
{item.description && <p>{item.description}</p>}
{/* Status-specific information */}
<div className="flex items-center gap-2">
{item.status === GiftStatus.AVAILABLE && (
<>
{item.store_name && (
<span>Store: {item.store_name}</span>
)}
{item.purchase_url && (
<>
{item.store_name && <span></span>}
<a
href={item.purchase_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Shop Link
</a>
</>
)}
{!item.store_name &&
!item.purchase_url &&
item.brand && (
<span>Brand: {item.brand}</span>
)}
{!item.store_name &&
!item.purchase_url &&
!item.brand && (
<span className={"italic"}>
No purchase information available
</span>
)}
</>
)}
{item.status === GiftStatus.RESERVED && (
<span>Reserved</span>
)}
{item.status === GiftStatus.PURCHASED && (
<span>Purchased</span>
)}
{item.status === GiftStatus.RECEIVED && (
<span>Received</span>
)}
</div>
</div>
{item.description}
</TableCell>
</TableRow>
{/*)}*/}
</React.Fragment>
);
})

View File

@@ -27,11 +27,13 @@ import {
import { useEventThemes } from "@/context/event-theme-context";
import { getServerFileUrl } from "@/lib/utils";
import GuestsList from "@/components/guests/guests-list";
import { useGuests } from "@/context/guest-context";
export default function EventDetailPage() {
const { slug } = useParams<{ slug: string }>();
const { event, fetchEventBySlug, isLoadingEvent, eventError } = useEvents();
const { themes } = useEventThemes();
const { refetchGuests } = useGuests();
const currentTheme =
event?.theme_id && themes
? themes.find((theme) => theme.id === event.theme_id)
@@ -44,6 +46,10 @@ export default function EventDetailPage() {
fetchEventBySlug(slug);
}, [slug, fetchEventBySlug]);
useEffect(() => {
refetchGuests();
}, []);
if (isLoadingEvent) {
return (
<div className="flex items-center justify-center min-h-[50vh]">

View File

@@ -5,6 +5,7 @@ import { useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
import Navbar from "@/components/layout/navbar";
import Breadcrumbs from "@/components/layout/breadcrumb";
import { useEvents } from "@/context/event-context";
export default function MainLayout({
children,
@@ -12,12 +13,18 @@ export default function MainLayout({
children: React.ReactNode;
}) {
const { isAuthenticated, isLoading } = useAuth();
const { refetchUpcomingEvents, refetchPublicEvents, refetchUserEvents } =
useEvents();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push("/login");
} else {
refetchUpcomingEvents();
refetchPublicEvents();
refetchUserEvents();
}
}, [isAuthenticated, isLoading, router]);

View File

@@ -48,6 +48,7 @@ import {
readGiftPurchase,
readGiftPurchasesByGift,
readGiftPurchasesByGuest,
readGuestsGiftPurchases,
createEvent,
getUserEvents,
getUpcomingEvents,
@@ -165,6 +166,7 @@ import type {
ReadGiftPurchaseData,
ReadGiftPurchasesByGiftData,
ReadGiftPurchasesByGuestData,
ReadGuestsGiftPurchasesData,
CreateEventData,
CreateEventError,
CreateEventResponse,
@@ -1421,6 +1423,27 @@ export const readGiftPurchasesByGuestOptions = (
});
};
export const readGuestsGiftPurchasesQueryKey = (
options?: Options<ReadGuestsGiftPurchasesData>,
) => createQueryKey("readGuestsGiftPurchases", options);
export const readGuestsGiftPurchasesOptions = (
options?: Options<ReadGuestsGiftPurchasesData>,
) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await readGuestsGiftPurchases({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: readGuestsGiftPurchasesQueryKey(options),
});
};
export const createEventQueryKey = (options: Options<CreateEventData>) =>
createQueryKey("createEvent", options);

View File

@@ -2715,6 +2715,16 @@ export const PresignedUrlResponseSchema = {
export const RSVPSchemaSchema = {
properties: {
event_id: {
type: "string",
format: "uuid",
title: "Event Id",
},
guest_id: {
type: "string",
format: "uuid",
title: "Guest Id",
},
status: {
$ref: "#/components/schemas/RSVPStatus",
},
@@ -2771,12 +2781,30 @@ export const RSVPSchemaSchema = {
},
},
type: "object",
required: ["status", "id", "response_date", "created_at", "updated_at"],
required: [
"event_id",
"guest_id",
"status",
"id",
"response_date",
"created_at",
"updated_at",
],
title: "RSVPSchema",
} as const;
export const RSVPSchemaCreateSchema = {
properties: {
event_id: {
type: "string",
format: "uuid",
title: "Event Id",
},
guest_id: {
type: "string",
format: "uuid",
title: "Guest Id",
},
status: {
$ref: "#/components/schemas/RSVPStatus",
},
@@ -2811,19 +2839,9 @@ export const RSVPSchemaCreateSchema = {
additional_info: {
title: "Additional Info",
},
event_id: {
type: "string",
format: "uuid",
title: "Event Id",
},
guest_id: {
type: "string",
format: "uuid",
title: "Guest Id",
},
},
type: "object",
required: ["status", "event_id", "guest_id"],
required: ["event_id", "guest_id", "status"],
title: "RSVPSchemaCreate",
} as const;

View File

@@ -142,6 +142,8 @@ import type {
ReadGiftPurchasesByGuestData,
ReadGiftPurchasesByGuestResponse,
ReadGiftPurchasesByGuestError,
ReadGuestsGiftPurchasesData,
ReadGuestsGiftPurchasesResponse,
CreateEventData,
CreateEventResponse,
CreateEventError,
@@ -1153,6 +1155,23 @@ export const readGiftPurchasesByGuest = <ThrowOnError extends boolean = false>(
});
};
/**
* Read All Guest Gift Reservations
* Retrieve all guest gift reservations.
*/
export const readGuestsGiftPurchases = <ThrowOnError extends boolean = false>(
options?: Options<ReadGuestsGiftPurchasesData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
ReadGuestsGiftPurchasesResponse,
unknown,
ThrowOnError
>({
url: "/api/v1/events/gifts/purchases/guest/all",
...options,
});
};
/**
* Create Event
* Create a new event.

View File

@@ -467,6 +467,8 @@ export type PresignedUrlResponse = {
};
export type RsvpSchema = {
event_id: string;
guest_id: string;
status: RsvpStatus;
number_of_guests?: number;
response_message?: string | null;
@@ -479,13 +481,13 @@ export type RsvpSchema = {
};
export type RsvpSchemaCreate = {
event_id: string;
guest_id: string;
status: RsvpStatus;
number_of_guests?: number;
response_message?: string | null;
dietary_requirements?: string | null;
additional_info?: unknown;
event_id: string;
guest_id: string;
};
export type RsvpSchemaUpdate = {
@@ -1874,6 +1876,23 @@ export type ReadGiftPurchasesByGuestResponses = {
export type ReadGiftPurchasesByGuestResponse =
ReadGiftPurchasesByGuestResponses[keyof ReadGiftPurchasesByGuestResponses];
export type ReadGuestsGiftPurchasesData = {
body?: never;
path?: never;
query?: never;
url: "/api/v1/events/gifts/purchases/guest/all";
};
export type ReadGuestsGiftPurchasesResponses = {
/**
* Successful Response
*/
200: Array<GiftPurchase>;
};
export type ReadGuestsGiftPurchasesResponse =
ReadGuestsGiftPurchasesResponses[keyof ReadGuestsGiftPurchasesResponses];
export type CreateEventData = {
body: EventCreate;
path?: never;

View File

@@ -39,7 +39,6 @@ import {
import { useGuests } from "@/context/guest-context";
import {
EventResponse,
EventThemeResponse,
GuestCreate,
GuestRead,
GuestStatus,
@@ -66,6 +65,12 @@ import {
} from "@/components/ui/alert-dialog";
import { useAuth } from "@/context/auth-context";
import { generateInviteLink } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { StickyNote, Utensils } from "lucide-react";
// Helper to generate a random invitation code
const generateInvitationCode = (fullName: string): string => {
@@ -522,10 +527,13 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
<TableHead>Phone</TableHead>
<TableHead>Invitation Code</TableHead>
<TableHead>Status</TableHead>
<TableHead>Additional Guests</TableHead>
<TableHead>Add. Guests</TableHead>
<TableHead>Diet Restr.</TableHead>
<TableHead>Notes</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoadingGuests ? (
<TableRow>
@@ -567,6 +575,54 @@ const GuestListTable = ({ event }: GuestListTableProps) => {
</TableCell>
<TableCell>{getStatusBadge(guest.status)}</TableCell>
<TableCell>{guest.actual_additional_guests || 0}</TableCell>
{/* Dietary Restrictions Column */}
<TableCell>
{guest.dietary_restrictions &&
guest.dietary_restrictions.length > 0 ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
>
<Utensils className="h-4 w-4 text-green-600 cursor-pointer" />
</Button>
</PopoverTrigger>
<PopoverContent className="max-w-xs">
<p className="text-sm">
{guest.dietary_restrictions}
</p>
</PopoverContent>
</Popover>
) : (
"-"
)}
</TableCell>
{/* Notes Column */}
<TableCell>
{guest.notes && guest.notes.length > 0 ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
>
<StickyNote className="h-4 w-4 text-blue-600 cursor-pointer" />
</Button>
</PopoverTrigger>
<PopoverContent className="max-w-xs">
<p className="text-sm">{guest.notes}</p>
</PopoverContent>
</Popover>
) : (
"-"
)}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -19,6 +19,7 @@ import { useSearchParams, useParams } from "next/navigation";
import { Loader2, Plus, Minus } from "lucide-react";
import { motion } from "framer-motion";
import { useTheme } from "next-themes";
import { useRSVPs } from "@/context/rsvp-context";
interface RSVPProps {
eventId: string;
@@ -32,6 +33,8 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
findGuestByInvitationCode,
submitGuestRsvp,
} = useGuests();
const { rsvps } = useRSVPs();
const searchParams = useSearchParams();
const { slug } = useParams<{ slug: string }>();
const { event, fetchEventBySlug, isLoadingEvent } = useEvents();
@@ -45,6 +48,9 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<boolean>(false);
const [currentGuest, setCurrentGuest] = useState<any | null>(null);
const [currentRSVP, setCurrentRSVP] = useState<any | null>(null);
const { setTheme } = useTheme();
useEffect(() => {
@@ -58,6 +64,21 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
}
}, [slug, fetchEventBySlug]);
useEffect(() => {
if (rsvps && currentGuest) {
setCurrentRSVP(rsvps.find((rsvp) => rsvp.guest_id === currentGuest?.id));
}
}, [rsvps, currentGuest]);
useEffect(() => {
if (currentRSVP) {
setStatus(currentRSVP.status);
setNumberOfGuests(currentRSVP.number_of_guests);
setResponseMessage(currentRSVP.response_message);
setDietaryRequirements(currentRSVP.dietary_requirements);
}
}, [currentRSVP]);
// Find the theme for this event
const eventTheme =
event && themes?.find((theme) => theme.id === event.theme_id);
@@ -114,7 +135,7 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
// Find the guest with matching invitation code
if (guests) {
const matchingGuest = findGuestByInvitationCode(invitationCode);
setCurrentGuest(matchingGuest);
if (matchingGuest) {
console.log("matchingGuest ", matchingGuest);
setGuestId(matchingGuest.id);
@@ -248,13 +269,12 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
if (success) {
return (
<motion.div
className="w-full rounded-lg border shadow-sm p-8 text-center"
className="w-full rounded-lg shadow-sm p-8 text-center"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
style={{
backgroundColor: colors.background,
borderColor: colors.primary,
color: colors.text,
}}
>
@@ -298,13 +318,12 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
return (
<motion.div
className="w-full rounded-lg border shadow-sm"
className="w-full rounded-lg shadow-sm"
initial="hidden"
animate="visible"
variants={formVariants}
style={{
backgroundColor: colors.background,
borderColor: colors.backgroundDark,
color: colors.text,
}}
>
@@ -466,12 +485,15 @@ export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
<motion.div className="flex justify-end mt-6" variants={itemVariants}>
<Button
type="submit"
className="w-full md:w-auto"
className="w-full md:w-auto text-white transition-colors duration-300"
disabled={isProcessing}
style={{
backgroundColor: colors.accent,
color: "white",
}}
style={{ backgroundColor: colors.accent }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = colors.accent2)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = colors.accent)
}
>
{isProcessing ? (
<>

View File

@@ -32,6 +32,11 @@ interface EventsContextState {
userEvents: PaginatedResponseEventResponse | undefined;
upcomingEvents: PaginatedResponseEventResponse | undefined;
publicEvents: PaginatedResponseEventResponse | undefined;
refetchUpcomingEvents: () => Promise<any>;
refetchPublicEvents: () => Promise<any>;
refetchUserEvents: () => Promise<any>;
isLoadingUserEvents: boolean;
isLoadingUpcomingEvents: boolean;
isLoadingPublicEvents: boolean;
@@ -67,6 +72,11 @@ const defaultEventsState: EventsContextState = {
userEvents: undefined,
upcomingEvents: undefined,
publicEvents: undefined,
refetchUpcomingEvents: async () => undefined,
refetchPublicEvents: async () => undefined,
refetchUserEvents: async () => undefined,
isLoadingUserEvents: false,
isLoadingUpcomingEvents: false,
isLoadingPublicEvents: false,
@@ -148,6 +158,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
data: userEvents,
isLoading: isLoadingUserEvents,
error: userEventsError,
refetch: refetchUserEvents,
} = useQuery({
...getUserEventsOptions(paginationParams),
});
@@ -157,6 +168,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
data: upcomingEvents,
isLoading: isLoadingUpcomingEvents,
error: upcomingEventsError,
refetch: refetchUpcomingEvents,
} = useQuery({
...getUpcomingEventsOptions(paginationParams),
});
@@ -166,6 +178,7 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
data: publicEvents,
isLoading: isLoadingPublicEvents,
error: publicEventsError,
refetch: refetchPublicEvents,
} = useQuery({
...getPublicEventsOptions(paginationParams),
});
@@ -255,6 +268,11 @@ export const EventsProvider: React.FC<EventsProviderProps> = ({ children }) => {
userEvents,
upcomingEvents,
publicEvents,
refetchPublicEvents,
refetchUpcomingEvents,
refetchUserEvents,
isLoadingUserEvents,
isLoadingUpcomingEvents,
isLoadingPublicEvents,