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)
|
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>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
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 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
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[]
|
eventGuests: any[]
|
||||||
todos: Todo[]
|
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