event rsvp popup

This commit is contained in:
2025-07-05 20:29:09 -04:00
parent 2d62fb7d48
commit de40c78f47
7 changed files with 282 additions and 14 deletions

View File

@@ -6,19 +6,17 @@ export default async function SingleEventPage({ params }: { params: { eventId: s
const data = await queries.singleEvent(params.eventId) const data = await queries.singleEvent(params.eventId)
return ( return (
<> <div className=''>
<EventDashboard
event={data}
/>
{/* <div className=''>
{data ? ( {data ? (
// @ts-ignore <EventDashboard
<EventInfoDisplay event={data} /> // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
event={data}
/>
) : ( ) : (
<p className="text-center text-gray-500 mt-10">Event not found.</p> <p className="text-center text-gray-500 mt-10">Event not found.</p>
)} )}
</div> */} </div>
</>
) )
} }

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Wedding Planner", title: "Wedding Planner",
@@ -18,6 +19,7 @@ export default async function RootLayout({
className="bg-brand-background text-brand-text" className="bg-brand-background text-brand-text"
> >
{children} {children}
<Toaster />
</body> </body>
</html> </html>
); );

View File

@@ -3,7 +3,6 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import EventInfo from './EventInfo' import EventInfo from './EventInfo'
import EventRsvpTracking from './EventRsvpTracking' import EventRsvpTracking from './EventRsvpTracking'
import EventToDoList from './EventToDoList'
import ToDoList from '../ToDoList' import ToDoList from '../ToDoList'
import { fetchEventTodos } from '@/lib/helper/fetchTodos' import { fetchEventTodos } from '@/lib/helper/fetchTodos'

View File

@@ -0,0 +1,97 @@
'use client'
import React from 'react'
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'
import { toast } from 'sonner'
import { Button } from '../ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
interface GuestBookEntry {
id: string
fName: string
lName: string
}
interface EventGuest {
id: string
rsvp: 'YES' | 'NO' | 'PENDING'
eventId: string
guestBookEntryId: string
guestBookEntry: GuestBookEntry
}
interface Props {
eventGuests: EventGuest[]
}
export default function EventRsvpModal({ eventGuests }: Props) {
const handleRsvpChange = async (
guest: EventGuest,
rsvp: 'YES' | 'NO' | 'PENDING'
) => {
try {
const res = await fetch(
`/api/events/${guest.eventId}/guests/${guest.guestBookEntryId}/rsvp`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rsvp })
}
)
if (!res.ok) throw new Error('Failed to update RSVP')
toast.success(`RSVP updated for ${guest.guestBookEntry.fName} ${guest.guestBookEntry.lName}`)
} catch (err) {
console.error('RSVP update error:', err)
toast.error('Failed to update RSVP')
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Manage Guest RSVPs</Button>
</DialogTrigger>
<DialogContent className='max-w-2xl'>
<DialogHeader>
<DialogTitle>Manage Guest RSVPs</DialogTitle>
<DialogDescription>
Update RSVP statuses for each guest attending this event.
</DialogDescription>
</DialogHeader>
<div className='space-y-4 max-h-[400px] overflow-y-auto pr-1'>
{eventGuests.length && eventGuests.map(guest => (
<div key={guest.id} className="flex justify-between items-center bg-muted p-3 rounded-md">
<div>
<p className="font-medium">{guest.guestBookEntry.fName + " " + guest.guestBookEntry.lName}</p>
</div>
<Select
defaultValue={guest.rsvp}
onValueChange={(value) =>
handleRsvpChange(guest, value as 'YES' | 'NO' | 'PENDING')
}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="YES">Yes</SelectItem>
<SelectItem value="NO">No</SelectItem>
</SelectContent>
</Select>
</div>
))}
</div>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="ghost">Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,12 +1,30 @@
'use client'
import React from 'react' import React from 'react'
import { Card, CardContent } from '../ui/card' import { Card, CardContent } from '../ui/card'
import { Button } from '../ui/button' import EventRsvpModal from './EventRsvpModal'
export default function EventRsvpTracking({ eventGuests }: EventData) { interface GuestBookEntry {
id: string
fName: string
lName: string
}
interface EventGuest {
id: string
rsvp: 'YES' | 'NO' | 'PENDING'
eventId: string
guestBookEntryId: string
guestBookEntry: GuestBookEntry
}
interface Props {
eventGuests: EventGuest[]
}
export default function EventRsvpTracking({ eventGuests }: Props) {
const attendingGuests = eventGuests.filter((g) => g.rsvp === 'YES'); const attendingGuests = eventGuests.filter((g) => g.rsvp === 'YES');
const notAttendingGuests = eventGuests.filter((g) => g.rsvp === 'NO'); const notAttendingGuests = eventGuests.filter((g) => g.rsvp === 'NO');
const pendingGuests = eventGuests.filter((g) => g.rsvp === 'PENDING'); const pendingGuests = eventGuests.filter((g) => g.rsvp === 'PENDING');
return ( return (
<Card className='py-0'> <Card className='py-0'>
<CardContent className='p-4'> <CardContent className='p-4'>
@@ -29,7 +47,7 @@ export default function EventRsvpTracking({ eventGuests }: EventData) {
<p className='text-2xl font-bold'>{pendingGuests.length}</p> <p className='text-2xl font-bold'>{pendingGuests.length}</p>
</div> </div>
</div> </div>
<Button variant="secondary" className="mt-4 hover:cursor-pointer hover:bg-brand-primary-900">Manage Guest List</Button> <EventRsvpModal eventGuests={eventGuests} />
</CardContent> </CardContent>
</Card> </Card>
) )

143
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

11
types.d.ts vendored
View File

@@ -53,3 +53,14 @@ interface EventData {
eventGuests: any[] eventGuests: any[]
todos: Todo[] todos: Todo[]
} }
interface EventGuest {
id: string
guestId: string
rsvp: 'YES' | 'NO' | 'PENDING'
guest: {
fName: string
lName: string
email?: string | null
}
}