diff --git a/README.md b/README.md
index 0604def..9dc964f 100644
--- a/README.md
+++ b/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
- Details
- 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
- [ ] Guest accounts
- [ ] 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
- 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
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.
diff --git a/app/(auth)/events/page.tsx b/app/(auth)/events/page.tsx
index b50e4b3..c257ba4 100644
--- a/app/(auth)/events/page.tsx
+++ b/app/(auth)/events/page.tsx
@@ -12,7 +12,7 @@ export default async function EventsPage() {
{allEvents.length == 0 ? (
<>
- You don't have any events yet.
Create One!
+ You don't have any events yet.
Create One!
>
) : (
diff --git a/app/(auth)/guest-book/page.tsx b/app/(auth)/guest-book/page.tsx
new file mode 100644
index 0000000..9dc5905
--- /dev/null
+++ b/app/(auth)/guest-book/page.tsx
@@ -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 Unauthorized
+
+ const entries = await queries.fetchGuestBookEntries()
+
+ return
+}
diff --git a/app/api/guestbook/[id]/route.ts b/app/api/guestbook/[id]/route.ts
new file mode 100644
index 0000000..78f44fa
--- /dev/null
+++ b/app/api/guestbook/[id]/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/guestbook/add/route.ts b/app/api/guestbook/add/route.ts
new file mode 100644
index 0000000..6bfacd5
--- /dev/null
+++ b/app/api/guestbook/add/route.ts
@@ -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 })
+ }
+}
diff --git a/bun.lockb b/bun.lockb
index 81907f5..c2ebbb0 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/AddGuestBookEntryModal.tsx b/components/AddGuestBookEntryModal.tsx
new file mode 100644
index 0000000..fc76a37
--- /dev/null
+++ b/components/AddGuestBookEntryModal.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ Add Guest Entry
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/EditGuestBookEntryModal.tsx b/components/EditGuestBookEntryModal.tsx
new file mode 100644
index 0000000..8b8ad94
--- /dev/null
+++ b/components/EditGuestBookEntryModal.tsx
@@ -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) {
+ setFormData(prev => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }))
+ }
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ onSubmit(formData)
+ onClose()
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/GuestBookList.tsx b/components/GuestBookList.tsx
new file mode 100644
index 0000000..00ab3de
--- /dev/null
+++ b/components/GuestBookList.tsx
@@ -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(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 (
+
+
+
+
+
+ Name
+ Email
+ Phone
+ Address
+ Notes
+
+
+
+ {entries.map(entry => (
+
+ {entry.fName + ' ' + entry.lName} (Side: {entry.side})
+ {entry.email || 'N/A'}
+ {entry.phone || 'N/A'}
+ {entry.address || 'N/A'}
+ {entry.notes || 'N/A'}
+
+ ))}
+
+
+
+
+ {entries.map(entry => (
+
+
{entry.fName} {entry.lName}
+
Side: {entry.side}
+
Email: {entry.email || 'N/A'}
+
Phone: {entry.phone || 'N/A'}
+
Address: {entry.address || 'N/A'}
+
Notes: {entry.notes || 'N/A'}
+
setEditingEntry(entry)}
+ >
+ Edit
+
+
+ ))}
+
+
+ {editingEntry && (
+
+ )}
+
+ )
+}
diff --git a/components/GuestBookPageClient.tsx b/components/GuestBookPageClient.tsx
new file mode 100644
index 0000000..fe39cf3
--- /dev/null
+++ b/components/GuestBookPageClient.tsx
@@ -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 (
+
+
+
Guest Book
+ setIsOpen(true)} className="btn btn-primary">Add Guest
+
+
+
+
+
setIsOpen(false)}
+ onSubmit={handleAddGuest}
+ />
+
+ )
+}
diff --git a/lib/mutations.ts b/lib/mutations.ts
index 197eb93..677e6d0 100644
--- a/lib/mutations.ts
+++ b/lib/mutations.ts
@@ -80,8 +80,41 @@ export const mutations = {
});
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 }
+ })
+ },
};
\ No newline at end of file
diff --git a/lib/queries.ts b/lib/queries.ts
index 5c22af9..815c6ba 100644
--- a/lib/queries.ts
+++ b/lib/queries.ts
@@ -27,5 +27,14 @@ export const queries = {
}
})
return event
- }
+ },
+
+ async fetchGuestBookEntries() {
+ return await prisma.guestBookEntry.findMany({
+ orderBy: [
+ { lName: 'asc' },
+ { fName: 'asc' }
+ ],
+ })
+ },
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 4e20437..ec38fad 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"prisma:migrate": "prisma migrate deploy"
},
"dependencies": {
+ "@headlessui/react": "^2.2.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@types/nodemailer": "^6.4.17",
"bcrypt": "^6.0.0",
diff --git a/prisma/migrations/20250626125351_add_guest_book/migration.sql b/prisma/migrations/20250626125351_add_guest_book/migration.sql
new file mode 100644
index 0000000..91c8d3a
--- /dev/null
+++ b/prisma/migrations/20250626125351_add_guest_book/migration.sql
@@ -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;
diff --git a/prisma/migrations/20250626133219_update_guestbook_side/migration.sql b/prisma/migrations/20250626133219_update_guestbook_side/migration.sql
new file mode 100644
index 0000000..f5a690b
--- /dev/null
+++ b/prisma/migrations/20250626133219_update_guestbook_side/migration.sql
@@ -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;
diff --git a/prisma/migrations/20250626150021_guest_book_fname_lname/migration.sql b/prisma/migrations/20250626150021_guest_book_fname_lname/migration.sql
new file mode 100644
index 0000000..e37a621
--- /dev/null
+++ b/prisma/migrations/20250626150021_guest_book_fname_lname/migration.sql
@@ -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;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6ba0c56..2e96698 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -60,3 +60,17 @@ model InviteToken {
accepted Boolean @default(false)
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())
+}
+
+