diff --git a/README.md b/README.md index 4c01563..f172af4 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,17 @@ My goal for this project is to be an all-in-one self hosted event planner for ma - [x] Invite users via email (smtp) users can be COUPLE, PLANNER, GUEST - [x] Create local accounts (no use of SMTP) - [x] Creating and Editing custom events - - [ ] Information about each event + - [x] Information about each event - Date/Time - Event type - Details - Location - [x] Guest book (contact information) - [x] Ability to switch between table or card view - - [ ] Add Guests to events + - [x] Add Guests to events - [ ] Invite guests via email - [ ] Create local account for guest -- [ ] Managing RSVP lists +- [x] Managing RSVP lists - [ ] Guest accounts - [ ] Gift Registries - [ ] Ability for guests to mark an item as purchased @@ -55,6 +55,12 @@ My goal for this project is to be an all-in-one self hosted event planner for ma - added ability to add and edit guests to guest book - save guest infomation (name, email, phone, address, side (which side of the couple), notes) +#### 6.28.25 +### RSVP List +- add guests from GuestBook to any event +- search GuestBook to add guests +- change status of RSVP + ## 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)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx index f1ce5db..428cd63 100644 --- a/app/(auth)/dashboard/page.tsx +++ b/app/(auth)/dashboard/page.tsx @@ -1,3 +1,7 @@ +import AddFirstGuestBookEntryClient from '@/components/AddFirstGuestBookEntryClient' +import AddGuestBookEntryModal from '@/components/AddGuestBookEntryModal' +import CreateEventClient from '@/components/CreateEventClient' +import DashboardNavbar from '@/components/DashboardNavbar' import EventInfoQuickView from '@/components/EventInfoQuickView' import GuestBookQuickView from '@/components/GuestBookQuickView' import { prisma } from '@/lib/prisma' @@ -9,32 +13,21 @@ import React from 'react' export default async function DashboardPage() { const events = await queries.fetchEvents(); const guestBookEntries = await queries.fetchGuestBookEntries(5); - const session = await getServerSession() + const session = await getServerSession(); const user = await prisma.user.findUnique({ where: { email: session?.user.email } }) - // console.log(events) return ( -
-
-
-
-

Hello, {user?.username}

-
-
Overview
-
-
+
+

Your Events

- +
+ {!events.length && <>You don't have any events yet. Create your first event.}
{events.map((item) => ( @@ -45,7 +38,7 @@ export default async function DashboardPage() { View all
-
+

Guest Book

+ {!guestBookEntries.length && } {guestBookEntries.map(entry => ( ))}
-
- {/*
-

Your Events

- {events.map((item) => ( - -
-

ID: {item.id}

-

Name: {item.name}

-

Date: {item.date ? item.date.toISOString() : 'null'}

-

Location: {item.location ? item.location : 'null'}

-

Created By: {item.creator.username}

-

Created At: {item.createdAt.toISOString()}

- -
- - ))} - - See all events - -
*/}
) } diff --git a/app/(auth)/events/[eventId]/page.tsx b/app/(auth)/events/[eventId]/page.tsx index 9adb68e..afa29f5 100644 --- a/app/(auth)/events/[eventId]/page.tsx +++ b/app/(auth)/events/[eventId]/page.tsx @@ -9,16 +9,13 @@ export default async function SingleEventPage({ params }: { params: { eventId: s console.log(data) return ( -
+
{data ? ( // @ts-ignore ) : (

Event not found.

)} - {data?.name && ( - - )}
) } diff --git a/app/(auth)/events/create/page.tsx b/app/(auth)/events/create/page.tsx deleted file mode 100644 index 70be375..0000000 --- a/app/(auth)/events/create/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; - -export default function CreateEventPage() { - const [name, setName] = useState(''); - const router = useRouter(); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - const res = await fetch('/api/events', { - method: 'POST', - body: JSON.stringify({ name }), - }); - const data = await res.json(); - router.push(`/events/${data.id}`); - } - - return ( -
-

Create Event

- setName(e.target.value)} - /> - -
- ); -} diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index 60035d8..ae9a5c4 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,19 +1,22 @@ 'use client' -import { SessionProvider, useSession } from 'next-auth/react' +import { SessionProvider } from 'next-auth/react' import { redirect } from 'next/navigation' import { ReactNode } from 'react' -import Navbar from '@/components/Navbar' +import DashboardNavbar from '@/components/DashboardNavbar' export default function AuthLayout({ children }: { children: ReactNode }) { return ( <> - -
- {/* Could also add a private header here */} - {children} +
+
+ +
+ {children} +
+
diff --git a/app/(public)/setup/page.tsx b/app/(public)/setup/page.tsx index 637c66b..82014e5 100644 --- a/app/(public)/setup/page.tsx +++ b/app/(public)/setup/page.tsx @@ -14,11 +14,11 @@ export default function SetupPage() { const res = await fetch('/api/setup', { method: 'POST', - body: JSON.stringify({ email, password, role }), + body: JSON.stringify({ username, email, password, role }), }); if (res.ok) { - await signIn('credentials', { email, password, callbackUrl: '/' }); + await signIn('credentials', { username, email, password, callbackUrl: '/' }); } else { alert('Error setting up user'); } diff --git a/app/api/events/[eventId]/guests/add/route.ts b/app/api/events/[eventId]/guests/add/route.ts index 2b5d8f5..8a1910b 100644 --- a/app/api/events/[eventId]/guests/add/route.ts +++ b/app/api/events/[eventId]/guests/add/route.ts @@ -3,14 +3,14 @@ import { mutations } from '@/lib/mutations' export async function POST(req: NextRequest, { params }: { params: { eventId: string } }) { const eventId = params.eventId - const { guestBookEntryId } = await req.json() + const { guestId } = await req.json() // ← match client - if (!eventId || !guestBookEntryId) { - return NextResponse.json({ message: 'Missing eventId or guestBookEntryId' }, { status: 400 }) + if (!eventId || !guestId) { + return NextResponse.json({ message: 'Missing eventId or guestId' }, { status: 400 }) } try { - const added = await mutations.addEventGuest({ eventId, guestBookEntryId }) + const added = await mutations.addEventGuest({ eventId, guestBookEntryId: guestId }) // ← match expected arg return NextResponse.json(added) } catch (error) { console.error('Add Event Guest Error:', error) diff --git a/app/api/events/create/route.ts b/app/api/events/create/route.ts new file mode 100644 index 0000000..d62a77d --- /dev/null +++ b/app/api/events/create/route.ts @@ -0,0 +1,24 @@ +import { getServerSession } from 'next-auth' +import { NextRequest, NextResponse } from 'next/server' +import { authOptions } from '@/app/api/auth/[...nextauth]/route' +import { prisma } from '@/lib/prisma' + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return new NextResponse('Unauthorized', { status: 403 }) + } + + const { name, date, location } = await req.json() + + const event = await prisma.event.create({ + data: { + name, + date: date ? new Date(date) : undefined, + location, + creatorId: session.user.id, + }, + }) + + return NextResponse.json(event) +} diff --git a/app/api/events/route.ts b/app/api/events/route.ts deleted file mode 100644 index c25ca9d..0000000 --- a/app/api/events/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getServerSession } from 'next-auth' -import { authOptions } from '@/app/api/auth/[...nextauth]/route' -import { mutations } from '@/lib/mutations'; -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(req: NextRequest) { - const session = await getServerSession(authOptions) - if (!session?.user) return new NextResponse('Unauthorized', { status: 401 }); - - const body = await req.json(); - const { name, date, location } = body; - - const event = await mutations.createEvent({ - name, - date, - location, - creatorId: session.user.id, - }); - - return NextResponse.json(event) -} \ No newline at end of file diff --git a/app/api/guestbook/search/route.ts b/app/api/guestbook/search/route.ts new file mode 100644 index 0000000..4e1b265 --- /dev/null +++ b/app/api/guestbook/search/route.ts @@ -0,0 +1,29 @@ +// api/guestbook/search/route.ts +import { prisma } from '@/lib/prisma' +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url) + const query = searchParams.get('search') // ✅ Match the client-side key + + if (!query || query.length < 2) { + return NextResponse.json([], { status: 200 }) + } + + const results = await prisma.guestBookEntry.findMany({ + where: { + OR: [ + { fName: { contains: query, mode: 'insensitive' } }, + { lName: { contains: query, mode: 'insensitive' } }, + { email: { contains: query, mode: 'insensitive' } }, + ] + }, + orderBy: [ + { lName: 'asc' }, + { fName: 'asc' }, + ], + take: 10 + }) + + return NextResponse.json(results) +} diff --git a/components/AddFirstGuestBookEntryClient.tsx b/components/AddFirstGuestBookEntryClient.tsx new file mode 100644 index 0000000..89ec18c --- /dev/null +++ b/components/AddFirstGuestBookEntryClient.tsx @@ -0,0 +1,24 @@ +'use client' +import React, { useState } from 'react' +import AddGuestBookEntryModal from './AddGuestBookEntryModal'; +import { handleAddGuest } from '@/lib/helper/addGuest'; + +export default function AddFirstGuestBookEntryClient() { + const [isOpen, setIsOpen] = useState(false); + return ( +
+

You haven't added anyone to your guest book yet.

+ + setIsOpen(false)} + onSubmit={handleAddGuest} + /> +
+ ) +} diff --git a/components/AddGuestFromGuestBook.tsx b/components/AddGuestFromGuestBook.tsx new file mode 100644 index 0000000..b9621ce --- /dev/null +++ b/components/AddGuestFromGuestBook.tsx @@ -0,0 +1,113 @@ +'use client' + +import React, { useEffect, useState } from 'react' + +interface GuestBookEntry { + id: string + fName: string + lName: string + email?: string | null + side?: string +} + +interface Props { + eventId: string + onGuestAdded?: (guestId: string) => void +} + +export default function AddGuestFromGuestBook({ eventId, onGuestAdded }: Props) { + const [searchTerm, setSearchTerm] = useState('') + const [filteredGuests, setFilteredGuests] = useState([]) + const [allGuests, setAllGuests] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (searchTerm.length < 2) { + setFilteredGuests([]) + return + } + + async function fetchGuests() { + setIsLoading(true) + try { + const res = await fetch(`/api/guestbook/search?search=${encodeURIComponent(searchTerm)}`) + const data = await res.json() + setAllGuests(data) + } catch (err) { + console.error(err) + setError('Failed to fetch guest book entries.') + } finally { + setIsLoading(false) + } + } + + fetchGuests() + }, [searchTerm]) + + + useEffect(() => { + const filtered = allGuests.filter((guest) => { + const fullName = `${guest.fName} ${guest.lName}`.toLowerCase() + return fullName.includes(searchTerm.toLowerCase()) + }) + setFilteredGuests(filtered) + }, [searchTerm, allGuests]) + + async function handleAdd(guestId: string) { + try { + const res = await fetch(`/api/events/${eventId}/guests/add`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guestId }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Failed to add guest to event') + } + + if (onGuestAdded) onGuestAdded(guestId) + + setSearchTerm('') + } catch (err) { + console.error(err) + setError('Could not add guest to event.') + } + } + + return ( +
+ setSearchTerm(e.target.value)} + /> + + {searchTerm && ( +
    + {isLoading ? ( +
  • Loading...
  • + ) : filteredGuests.length === 0 ? ( +
  • No matches found
  • + ) : ( + filteredGuests.map((guest) => ( +
  • handleAdd(guest.id)} + > + {guest.fName} {guest.lName} {guest.side ? `(${guest.side})` : ''} + {guest.email ? ` – ${guest.email}` : ''} +
  • + )) + )} +
+ )} + + {error &&

{error}

} +
+ ) +} diff --git a/components/CreateEventClient.tsx b/components/CreateEventClient.tsx new file mode 100644 index 0000000..fc2e1cd --- /dev/null +++ b/components/CreateEventClient.tsx @@ -0,0 +1,23 @@ +'use client' +import React, { useState } from 'react' +import CreateEventModal from './CreateEventModal' +import { handleCreateEvent } from '@/lib/helper/createEvent'; + +export default function CreateEventClient() { + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + setIsOpen(false)} + onSubmit={handleCreateEvent} + /> + + ) +} diff --git a/components/CreateEventModal.tsx b/components/CreateEventModal.tsx new file mode 100644 index 0000000..b758936 --- /dev/null +++ b/components/CreateEventModal.tsx @@ -0,0 +1,93 @@ +'use client' + +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' +import { Fragment, useState } from 'react' + +export default function CreateEventModal({ + isOpen, + onClose, + onSubmit +}: { + isOpen: boolean + onClose: () => void + onSubmit: (data: { name: string; date?: string; location?: string }) => void +}) { + const [name, setName] = useState('') + const [date, setDate] = useState('') + const [location, setLocation] = useState('') + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + onSubmit({ name, date, location }) + setName('') + setDate('') + setLocation('') + onClose() + } + + return ( + + + +
+ + +
+ + + Create New Event +
+ setName(e.target.value)} + required + /> + setDate(e.target.value)} + /> + setLocation(e.target.value)} + /> +
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/components/DashboardNavbar.tsx b/components/DashboardNavbar.tsx new file mode 100644 index 0000000..7878ea3 --- /dev/null +++ b/components/DashboardNavbar.tsx @@ -0,0 +1,19 @@ +'use client' +import { useSession } from 'next-auth/react' +import Link from 'next/link' +import React from 'react' + +export default function DashboardNavbar() { + const session = useSession() + console.log(session) + return ( +
+

Hello, {session.data?.user.username}

+
+ Overview + Events + Guest Book +
+
+ ) +} diff --git a/components/EventInfoDisplay.tsx b/components/EventInfoDisplay.tsx index 68cbce6..07afae5 100644 --- a/components/EventInfoDisplay.tsx +++ b/components/EventInfoDisplay.tsx @@ -3,6 +3,7 @@ 'use client' import React, { useState } from 'react' +import AddGuestFromGuestBook from './AddGuestFromGuestBook' interface Creator { id: string @@ -20,6 +21,7 @@ interface EventData { createdAt: string creator: Creator guests: any[] + eventGuests: any[] } interface Props { @@ -27,7 +29,11 @@ interface Props { } export default function EventInfoDisplay({ event }: Props) { - const [isEditing, setIsEditing] = useState(false) + const [isEditing, setIsEditing] = useState(false); + const [showSearch, setShowSearch] = useState(false); + + const eventGuests = event.eventGuests + console.log(eventGuests) const [saving, setSaving] = useState(false) const [error, setError] = useState('') @@ -75,98 +81,160 @@ export default function EventInfoDisplay({ event }: Props) { } } + // async function handleChangeRsvp(e: any) { + // const newRsvp = e.target.value as '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: newRsvp }), + // } + // ); + + // if (!res.ok) { + // throw new Error('Failed to update RSVP'); + // } + + // // Optionally trigger re-fetch or state update here + // } catch (err) { + // console.error('RSVP update error:', err); + // // Optionally show error message in UI + // } + // } + function formatDate(date: string) { const d = new Date(date) return `${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` } return ( -
-
-

Event Info

- -
- -
- {/* Event Name */} -
- - {isEditing ? ( - - ) : ( -

{event.name}

- )} -
- - {/* Event Date */} -
- - {isEditing ? ( - setDateTime(e.target.value)} - /> - ) : ( -

{event.date ? formatDate(event.date.toDateString()) : 'N/A'}

- )} -
- - {/* Location */} -
- - {isEditing ? ( - - ) : ( -

{event.location || 'N/A'}

- )} -
- - {/* Creator Email */} -
- -

{event.creator.email}

-
- - {/* Created At */} -
- -

{formatDate(event.createdAt)}

-
-
- - {error &&

{error}

} - - {isEditing && ( -
+
+
+
+

{event.name}

- )} + +
+ {/* Event Date */} +
+ + {isEditing ? ( + setDateTime(e.target.value)} + /> + ) : ( +

{event.date ? formatDate(event.date.toDateString()) : 'N/A'}

+ )} +
+ + {/* Location */} +
+ + {isEditing ? ( + + ) : ( +

{event.location || 'N/A'}

+ )} +
+ + {/* Creator Email */} +
+ +

{event.creator.email}

+
+ + {/* Created At */} +
+ +

{formatDate(event.createdAt)}

+
+
+ {error &&

{error}

} + {isEditing && ( +
+ +
+ )} +
+
+
+

Guest List

+ +
+ {showSearch && } +
    + {eventGuests.length && eventGuests.map(guest => ( +
  • +

    {guest.guestBookEntry.fName + " " + guest.guestBookEntry.lName}

    + +
  • + ))} +
+ +
+
+ Vendors +
) } diff --git a/lib/helper/addGuest.ts b/lib/helper/addGuest.ts new file mode 100644 index 0000000..c916f95 --- /dev/null +++ b/lib/helper/addGuest.ts @@ -0,0 +1,26 @@ +export 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) + } +} \ No newline at end of file diff --git a/lib/helper/createEvent.ts b/lib/helper/createEvent.ts new file mode 100644 index 0000000..cfcc675 --- /dev/null +++ b/lib/helper/createEvent.ts @@ -0,0 +1,23 @@ +export async function handleCreateEvent(data: { + name: string + date?: string + location?: string +}) { + try { + const res = await fetch('/api/events/create', { + 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 create event') + } + + // Optionally return or mutate data + return await res.json() + } catch (err) { + console.error('Error creating event:', err) + } +} diff --git a/lib/queries.ts b/lib/queries.ts index e162dd1..98a5d3d 100644 --- a/lib/queries.ts +++ b/lib/queries.ts @@ -12,7 +12,7 @@ export const queries = { } } }) - console.log(allEvents) + return allEvents; }, @@ -65,7 +65,12 @@ export const queries = { creator: { select: { id: true, email: true, name: true, role: true }, }, - guests: true + guests: true, + eventGuests: { + include: { + guestBookEntry: true, + } + }, } }) return event