guest book added
This commit is contained in:
13
README.md
13
README.md
@@ -18,7 +18,11 @@ My goal for this project is to be an all-in-one self hosted event planner for ma
|
|||||||
- Event type
|
- Event type
|
||||||
- Details
|
- Details
|
||||||
- Location
|
- Location
|
||||||
- [ ] Guest book (contact information)
|
- [x] Guest book (contact information)
|
||||||
|
- [ ] Ability to switch between table or card view
|
||||||
|
- [ ] Add Guests to events
|
||||||
|
- [ ] Invite guests via email
|
||||||
|
- [ ] Create local account for guest
|
||||||
- [ ] Managing RSVP lists
|
- [ ] Managing RSVP lists
|
||||||
- [ ] Guest accounts
|
- [ ] Guest accounts
|
||||||
- [ ] Gift Registries
|
- [ ] Gift Registries
|
||||||
@@ -44,6 +48,13 @@ My goal for this project is to be an all-in-one self hosted event planner for ma
|
|||||||
#### 6.25.25
|
#### 6.25.25
|
||||||
- now able to see and edit event data from the individual event page
|
- now able to see and edit event data from the individual event page
|
||||||
|
|
||||||
|
#### 6.26.25
|
||||||
|
### The Guest Book
|
||||||
|
- added guest-book page, viewable by PLANNER and COUPLE accounts
|
||||||
|
- db query is secure behind PLANNER and COUPLE auth sessions
|
||||||
|
- added ability to add and edit guests to guest book
|
||||||
|
- save guest infomation (name, email, phone, address, side (which side of the couple), notes)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
This is very much a work in progress but this `README` will stay up to date on working features and steps to get it running **in its current state**. That being said if you're interested in starting it as is, you can follow these instructions.
|
This is very much a work in progress but this `README` will stay up to date on working features and steps to get it running **in its current state**. That being said if you're interested in starting it as is, you can follow these instructions.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default async function EventsPage() {
|
|||||||
<div>
|
<div>
|
||||||
{allEvents.length == 0 ? (
|
{allEvents.length == 0 ? (
|
||||||
<>
|
<>
|
||||||
You don't have any events yet. <Link href={'/events/create'} className='underline'>Create One!</Link>
|
You don't have any events yet. <Link href={'/events/create'} className='underline'>Create One!</Link>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<table className='table-auto w-full'>
|
<table className='table-auto w-full'>
|
||||||
|
|||||||
13
app/(auth)/guest-book/page.tsx
Normal file
13
app/(auth)/guest-book/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
|
||||||
|
import { queries } from '@/lib/queries'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import GuestBookPageClient from '@/components/GuestBookPageClient'
|
||||||
|
|
||||||
|
export default async function GuestBookPage() {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session?.user) return <p className='text-center mt-10'>Unauthorized</p>
|
||||||
|
|
||||||
|
const entries = await queries.fetchGuestBookEntries()
|
||||||
|
|
||||||
|
return <GuestBookPageClient entries={entries} />
|
||||||
|
}
|
||||||
20
app/api/guestbook/[id]/route.ts
Normal file
20
app/api/guestbook/[id]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { mutations } from '@/lib/mutations'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '../../auth/[...nextauth]/route'
|
||||||
|
|
||||||
|
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session?.user || !['COUPLE', 'PLANNER'].includes(session.user.role)) {
|
||||||
|
return new NextResponse('Unauthorized', { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await req.json()
|
||||||
|
const updated = await mutations.updateGuestBookEntry(params.id, data)
|
||||||
|
return NextResponse.json(updated)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[EDIT GUESTBOOK ENTRY]', err)
|
||||||
|
return new NextResponse('Failed to update guestbook entry', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/api/guestbook/add/route.ts
Normal file
20
app/api/guestbook/add/route.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { mutations } from '@/lib/mutations'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '../../auth/[...nextauth]/route'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session?.user || !['COUPLE', 'PLANNER'].includes(session.user.role)) {
|
||||||
|
return new NextResponse('Unauthorized', { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await req.json()
|
||||||
|
const entry = await mutations.createGuestBookEntry(data)
|
||||||
|
return NextResponse.json(entry)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ADD GUESTBOOK ENTRY]', err)
|
||||||
|
return new NextResponse('Failed to create guestbook entry', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
127
components/AddGuestBookEntryModal.tsx
Normal file
127
components/AddGuestBookEntryModal.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use client'
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
|
||||||
|
import { Fragment, useState } from 'react'
|
||||||
|
|
||||||
|
export default function AddGuestBookEntryModal({ isOpen, onClose, onSubmit }: {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (data: { fName: string, lName: string, email: string, phone?: string, address?: string, side: string, notes?: string }) => void
|
||||||
|
}) {
|
||||||
|
const [fName, setFName] = useState('');
|
||||||
|
const [lName, setLName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [address, setAddress] = useState('');
|
||||||
|
const [side, setSide] = useState('');
|
||||||
|
const [notes, setNotes] = useState('');
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit({ fName, lName, email, phone, address, side, notes })
|
||||||
|
setFName('')
|
||||||
|
setLName('')
|
||||||
|
setEmail('')
|
||||||
|
setPhone('')
|
||||||
|
setAddress('')
|
||||||
|
setSide('')
|
||||||
|
setNotes('')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-30" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel className="w-full max-w-lg transform overflow-hidden rounded bg-white p-6 text-left shadow-xl transition-all">
|
||||||
|
<DialogTitle className="text-lg font-bold mb-4">
|
||||||
|
Add Guest Entry
|
||||||
|
</DialogTitle>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="First name"
|
||||||
|
value={fName}
|
||||||
|
onChange={e => setFName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Last name"
|
||||||
|
value={lName}
|
||||||
|
onChange={e => setLName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="tel"
|
||||||
|
placeholder="Phone"
|
||||||
|
value={phone}
|
||||||
|
onChange={e => setPhone(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Address"
|
||||||
|
value={address}
|
||||||
|
onChange={e => setAddress(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Side (e.g., Bride/Groom)"
|
||||||
|
value={side}
|
||||||
|
onChange={e => setSide(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder='Notes (e.g. Family/Friend of ...)'
|
||||||
|
value={notes}
|
||||||
|
onChange={e => setNotes(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button type="button" onClick={onClose} className="btn btn-outline">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
138
components/EditGuestBookEntryModal.tsx
Normal file
138
components/EditGuestBookEntryModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
|
||||||
|
import { Fragment, useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function EditGuestBookEntryModal({ isOpen, onClose, initialData, onSubmit }: {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
initialData: {
|
||||||
|
id: string
|
||||||
|
fName: string
|
||||||
|
lName: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
side?: string
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
onSubmit: (updated: typeof initialData) => void
|
||||||
|
}) {
|
||||||
|
const [formData, setFormData] = useState(initialData)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData(initialData)
|
||||||
|
}, [initialData])
|
||||||
|
|
||||||
|
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(formData)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-30" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel className="w-full max-w-lg transform overflow-hidden rounded bg-white p-6 text-left shadow-xl transition-all">
|
||||||
|
<DialogTitle className="text-lg font-bold mb-4">
|
||||||
|
Edit Guest Entry
|
||||||
|
</DialogTitle>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder='First Name'
|
||||||
|
value={formData.fName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder='Last Name'
|
||||||
|
value={formData.lName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="email"
|
||||||
|
placeholder='email'
|
||||||
|
name="email"
|
||||||
|
value={formData.email || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
name="address"
|
||||||
|
value={formData.address || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
name="side"
|
||||||
|
value={formData.side || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className='input input-bordered w-full'
|
||||||
|
name='notes'
|
||||||
|
value={formData.notes || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button type="button" onClick={onClose} className="btn btn-outline">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
components/GuestBookList.tsx
Normal file
127
components/GuestBookList.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import EditGuestBookEntryModal from './EditGuestBookEntryModal'
|
||||||
|
|
||||||
|
interface GuestBookEntry {
|
||||||
|
id: string
|
||||||
|
fName: string
|
||||||
|
lName: string
|
||||||
|
side: string
|
||||||
|
email?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
address?: string | null
|
||||||
|
notes?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GuestBookList({ entries }: { entries: GuestBookEntry[] }) {
|
||||||
|
const [editingEntry, setEditingEntry] = useState<GuestBookEntry | null>(null)
|
||||||
|
|
||||||
|
function handleModalClose() {
|
||||||
|
setEditingEntry(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdate(updated: {
|
||||||
|
id: string
|
||||||
|
fName: string
|
||||||
|
lName: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
side?: string
|
||||||
|
notes?: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/guestbook/${updated.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
fName: updated.fName,
|
||||||
|
lName: updated.lName,
|
||||||
|
email: updated.email,
|
||||||
|
phone: updated.phone,
|
||||||
|
address: updated.address,
|
||||||
|
side: updated.side,
|
||||||
|
notes: updated.notes,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const { message } = await res.json()
|
||||||
|
throw new Error(message || 'Update failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: trigger a state update/refetch if needed
|
||||||
|
setEditingEntry(null)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='overflow-hidden rounded-xl'>
|
||||||
|
<table className='table-auto w-full mb-16 p-4'>
|
||||||
|
<thead className='bg-text text-background'>
|
||||||
|
<tr className='text-left'>
|
||||||
|
<th className='px-4 py-2'>Name</th>
|
||||||
|
<th className='px-4 py-2'>Email</th>
|
||||||
|
<th className='px-4 py-2'>Phone</th>
|
||||||
|
<th className='px-4 py-2'>Address</th>
|
||||||
|
<th className='px-4 py-2'>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className=''>
|
||||||
|
{entries.map(entry => (
|
||||||
|
<tr
|
||||||
|
key={entry.id}
|
||||||
|
className=''
|
||||||
|
>
|
||||||
|
<td className='border border-gray-300 px-4 py-2'>{entry.fName + ' ' + entry.lName} <span className='text-sm text-gray-600'>(Side: {entry.side})</span></td>
|
||||||
|
<td className='border border-gray-300 px-4 py-2'>{entry.email || 'N/A'}</td>
|
||||||
|
<td className='border border-gray-300 px-4 py-2'>{entry.phone || 'N/A'}</td>
|
||||||
|
<td className='border border-gray-300 px-4 py-2'>{entry.address || 'N/A'}</td>
|
||||||
|
<td className='border border-gray-300 px-4 py-2'>{entry.notes || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className='max-w-3xl space-y-4 mx-auto'>
|
||||||
|
{entries.map(entry => (
|
||||||
|
<div key={entry.id} className='p-4 border rounded shadow-sm'>
|
||||||
|
<h2 className='font-semibold text-lg'>{entry.fName} {entry.lName}</h2>
|
||||||
|
<p className='text-sm text-gray-600'>Side: {entry.side}</p>
|
||||||
|
<p>Email: {entry.email || 'N/A'}</p>
|
||||||
|
<p>Phone: {entry.phone || 'N/A'}</p>
|
||||||
|
<p>Address: {entry.address || 'N/A'}</p>
|
||||||
|
<p>Notes: {entry.notes || 'N/A'}</p>
|
||||||
|
<button
|
||||||
|
className='text-blue-600 underline text-sm mt-2'
|
||||||
|
onClick={() => setEditingEntry(entry)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editingEntry && (
|
||||||
|
<EditGuestBookEntryModal
|
||||||
|
isOpen={!!editingEntry}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
initialData={{
|
||||||
|
id: editingEntry.id,
|
||||||
|
fName: `${editingEntry.fName}`,
|
||||||
|
lName: `${editingEntry.lName}`,
|
||||||
|
email: editingEntry.email || '',
|
||||||
|
phone: editingEntry.phone || '',
|
||||||
|
address: editingEntry.address || '',
|
||||||
|
side: editingEntry.side,
|
||||||
|
notes: editingEntry.notes || '',
|
||||||
|
}}
|
||||||
|
onSubmit={handleUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
components/GuestBookPageClient.tsx
Normal file
64
components/GuestBookPageClient.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import AddGuestBookEntryModal from '@/components/AddGuestBookEntryModal'
|
||||||
|
import GuestBookList from '@/components/GuestBookList'
|
||||||
|
|
||||||
|
interface GuestBookEntry {
|
||||||
|
id: string
|
||||||
|
fName: string
|
||||||
|
lName: string
|
||||||
|
side: string
|
||||||
|
email?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
address?: string | null
|
||||||
|
notes?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GuestBookPageClient({ entries }: { entries: GuestBookEntry[] }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
async function handleAddGuest(data: {
|
||||||
|
fName: string
|
||||||
|
lName: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
side: string
|
||||||
|
notes?: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/guestbook/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const { message } = await res.json()
|
||||||
|
throw new Error(message || 'Failed to add guest')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally: re-fetch entries or mutate state here
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error adding guest:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='max-w-6xl mx-auto mt-8 space-y-4'>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className='text-2xl font-bold'>Guest Book</h1>
|
||||||
|
<button onClick={() => setIsOpen(true)} className="btn btn-primary">Add Guest</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GuestBookList entries={entries} />
|
||||||
|
|
||||||
|
<AddGuestBookEntryModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
onSubmit={handleAddGuest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -80,8 +80,41 @@ export const mutations = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
async createGuestBookEntry(data: {
|
||||||
|
fName: string,
|
||||||
|
lName: string,
|
||||||
|
side: string,
|
||||||
|
email?: string,
|
||||||
|
phone?: string,
|
||||||
|
address?: string,
|
||||||
|
notes?: string
|
||||||
|
}) {
|
||||||
|
return await prisma.guestBookEntry.create({
|
||||||
|
data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateGuestBookEntry(id: string, data: Partial<{
|
||||||
|
fName: string,
|
||||||
|
lName: string,
|
||||||
|
side: string,
|
||||||
|
email?: string,
|
||||||
|
phone?: string,
|
||||||
|
address?: string,
|
||||||
|
notes?: string
|
||||||
|
}>){
|
||||||
|
return await prisma.guestBookEntry.update({
|
||||||
|
where: { id },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async deletGuestBookEntry(id: string) {
|
||||||
|
return await prisma.guestBookEntry.delete({
|
||||||
|
where: { id }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -27,5 +27,14 @@ export const queries = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return event
|
return event
|
||||||
}
|
},
|
||||||
|
|
||||||
|
async fetchGuestBookEntries() {
|
||||||
|
return await prisma.guestBookEntry.findMany({
|
||||||
|
orderBy: [
|
||||||
|
{ lName: 'asc' },
|
||||||
|
{ fName: 'asc' }
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"prisma:migrate": "prisma migrate deploy"
|
"prisma:migrate": "prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.4",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GuestBookEntry" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"ownerId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT,
|
||||||
|
"phone" TEXT,
|
||||||
|
"address" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "GuestBookEntry_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GuestBookEntry" ADD CONSTRAINT "GuestBookEntry_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `ownerId` on the `GuestBookEntry` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `side` to the `GuestBookEntry` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "GuestBookEntry" DROP CONSTRAINT "GuestBookEntry_ownerId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "GuestBookEntry" DROP COLUMN "ownerId",
|
||||||
|
ADD COLUMN "side" TEXT NOT NULL;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `name` on the `GuestBookEntry` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `fName` to the `GuestBookEntry` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `lName` to the `GuestBookEntry` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "GuestBookEntry" DROP COLUMN "name",
|
||||||
|
ADD COLUMN "fName" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "lName" TEXT NOT NULL;
|
||||||
@@ -60,3 +60,17 @@ model InviteToken {
|
|||||||
accepted Boolean @default(false)
|
accepted Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model GuestBookEntry {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
fName String
|
||||||
|
lName String
|
||||||
|
email String?
|
||||||
|
phone String?
|
||||||
|
address String?
|
||||||
|
notes String?
|
||||||
|
side String // e.g., "Brian", "Janice", etc.
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user