event rsvp popup
This commit is contained in:
@@ -6,19 +6,17 @@ export default async function SingleEventPage({ params }: { params: { eventId: s
|
||||
const data = await queries.singleEvent(params.eventId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<EventDashboard
|
||||
event={data}
|
||||
/>
|
||||
{/* <div className=''>
|
||||
<div className=''>
|
||||
|
||||
{data ? (
|
||||
// @ts-ignore
|
||||
<EventInfoDisplay event={data} />
|
||||
<EventDashboard
|
||||
// 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>
|
||||
)}
|
||||
</div> */}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Wedding Planner",
|
||||
@@ -18,6 +19,7 @@ export default async function RootLayout({
|
||||
className="bg-brand-background text-brand-text"
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import EventInfo from './EventInfo'
|
||||
import EventRsvpTracking from './EventRsvpTracking'
|
||||
import EventToDoList from './EventToDoList'
|
||||
import ToDoList from '../ToDoList'
|
||||
import { fetchEventTodos } from '@/lib/helper/fetchTodos'
|
||||
|
||||
|
||||
97
components/events/EventRsvpModal.tsx
Normal file
97
components/events/EventRsvpModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,30 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
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 notAttendingGuests = eventGuests.filter((g) => g.rsvp === 'NO');
|
||||
const pendingGuests = eventGuests.filter((g) => g.rsvp === 'PENDING');
|
||||
|
||||
return (
|
||||
<Card className='py-0'>
|
||||
<CardContent className='p-4'>
|
||||
@@ -29,7 +47,7 @@ export default function EventRsvpTracking({ eventGuests }: EventData) {
|
||||
<p className='text-2xl font-bold'>{pendingGuests.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="secondary" className="mt-4 hover:cursor-pointer hover:bg-brand-primary-900">Manage Guest List</Button>
|
||||
<EventRsvpModal eventGuests={eventGuests} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
143
components/ui/dialog.tsx
Normal file
143
components/ui/dialog.tsx
Normal 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
11
types.d.ts
vendored
@@ -53,3 +53,14 @@ interface EventData {
|
||||
eventGuests: any[]
|
||||
todos: Todo[]
|
||||
}
|
||||
|
||||
interface EventGuest {
|
||||
id: string
|
||||
guestId: string
|
||||
rsvp: 'YES' | 'NO' | 'PENDING'
|
||||
guest: {
|
||||
fName: string
|
||||
lName: string
|
||||
email?: string | null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user