added event todo list

This commit is contained in:
2025-06-29 11:00:52 -04:00
parent c7c121e23d
commit 11a0bb00e3
11 changed files with 342 additions and 63 deletions

View File

@@ -7,37 +7,40 @@ export default async function EventsPage() {
console.log(allEvents)
return (
<div>
Events
<div>
{allEvents.length == 0 ? (
<>
You don&apos;t have any events yet. <Link href={'/events/create'} className='underline'>Create One!</Link>
</>
) : (
<table className='table-auto w-full'>
<thead>
<tr>
<th>Event Name</th>
<th>Event Date</th>
<th>Created by</th>
</tr>
</thead>
<tbody>
{allEvents.map((item) => (
<tr
key={item.id}
className='text-center'
>
<td className=''><Link href={`/events/${item.id}`}>{item.name}</Link></td>
<td className=''>{item.date?.toDateString()}</td>
<td className=''>{item.creatorId}</td>
</tr>
))}
</tbody>
</table>
)}
<div className="max-w-7xl mx-auto px-4 py-10 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Your Events</h1>
<Link href="/events/create" className="btn btn-primary">
Create Event
</Link>
</div>
{allEvents.length === 0 ? (
<p className="text-lg text-gray-600">
You don&apos;t have any events yet.{' '}
<Link href="/events/create" className="underline text-brand-primary-600">
Create one!
</Link>
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{allEvents.map(event => (
<Link
href={`/events/${event.id}`}
key={event.id}
className="block bg-white border border-gray-200 hover:shadow-md rounded-lg p-5 transition-all"
>
<h2 className="text-xl font-semibold mb-1">{event.name}</h2>
<p className="text-sm text-gray-600">
{event.date ? new Date(event.date).toLocaleDateString() : 'No date set'}
</p>
<p className="text-sm text-gray-400 mt-2">
Created by: {event.creator?.username || 'Unknown'}
</p>
</Link>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,49 @@
// app/api/events/[eventId]/todos/[todoId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { mutations } from '@/lib/mutations';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export async function PATCH(
req: NextRequest,
{ params }: { params: { todoId: string; eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return new NextResponse('Unauthorized', { status: 401 });
}
const { name, dueDate, complete } = await req.json();
try {
const updated = await mutations.updateEventTodo(params.todoId, {
name,
dueDate,
complete,
});
return NextResponse.json(updated);
} catch (error) {
console.error('[UPDATE_TODO]', error);
return new NextResponse('Failed to update todo', { status: 500 });
}
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: { todoId: string; eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return new NextResponse('Unauthorized', { status: 401 });
}
try {
await mutations.deleteEventTodo(params.todoId);
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error('[DELETE_TODO]', error);
return new NextResponse('Failed to delete todo', { status: 500 });
}
}

View File

@@ -0,0 +1,33 @@
// app/api/events/[eventId]/todos/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { mutations } from '@/lib/mutations';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export async function POST(
req: NextRequest,
{ params }: { params: { eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return new NextResponse('Unauthorized', { status: 401 });
}
const { name, dueDate } = await req.json();
if (!name) {
return NextResponse.json({ message: 'Name is required' }, { status: 400 });
}
try {
const todo = await mutations.addTodoToEvent({
eventId: params.eventId,
name,
dueDate,
});
return NextResponse.json(todo);
} catch (error) {
console.error('[CREATE_TODO]', error);
return new NextResponse('Failed to create todo', { status: 500 });
}
}

View File

@@ -97,6 +97,7 @@ export default function EditGuestBookEntryModal({ isOpen, onClose, initialData,
className="input input-bordered w-full"
type="tel"
name="phone"
placeholder="phone"
value={formData.phone || ''}
onChange={handleChange}
/>
@@ -104,6 +105,7 @@ export default function EditGuestBookEntryModal({ isOpen, onClose, initialData,
className="input input-bordered w-full"
type="text"
name="address"
placeholder="address"
value={formData.address || ''}
onChange={handleChange}
/>
@@ -111,12 +113,14 @@ export default function EditGuestBookEntryModal({ isOpen, onClose, initialData,
className="input input-bordered w-full"
type="text"
name="side"
placeholder="Bride/Groom"
value={formData.side || ''}
onChange={handleChange}
/>
<textarea
className='input input-bordered w-full'
name='notes'
placeholder="notes"
value={formData.notes || ''}
onChange={handleChange}
/>

View File

@@ -5,6 +5,8 @@
import React, { useState } from 'react'
import AddGuestFromGuestBook from './AddGuestFromGuestBook'
import EventNotesEditor from './EventNotesEditor'
import ToDoList from './ToDoList'
import { fetchEventTodos } from '@/lib/helper/fetchTodos'
interface Creator {
id: string
@@ -13,6 +15,15 @@ interface Creator {
role: 'COUPLE' | 'PLANNER' | 'GUEST'
}
interface Todo {
id: string
name: string
complete: boolean
dueDate?: string | null
createdAt: string
updatedAt?: string
}
interface EventData {
id: string
name: string
@@ -24,6 +35,7 @@ interface EventData {
guests: any[]
notes?: string
eventGuests: any[]
todos: Todo[]
}
interface Props {
@@ -33,6 +45,7 @@ interface Props {
export default function EventInfoDisplay({ event }: Props) {
const [isEditing, setIsEditing] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const [todos, setTodos] = useState(event.todos)
const eventGuests = event.eventGuests
console.log(eventGuests)
@@ -83,29 +96,14 @@ 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
// }
// }
async function refreshTodos() {
try {
const data = await fetchEventTodos(event.id)
setTodos(data)
} catch (err) {
console.error('Failed to refresh todos:', err)
}
}
function formatDate(date: string) {
const d = new Date(date)
@@ -241,8 +239,12 @@ export default function EventInfoDisplay({ event }: Props) {
</ul>
</div>
<div>
Vendors
<div className='col-span-3'>
<ToDoList
eventId={event.id}
initialTodos={todos}
onUpdate={refreshTodos}
/>
</div>
</div>
)

117
components/ToDoList.tsx Normal file
View File

@@ -0,0 +1,117 @@
'use client'
import React, { useState } from 'react'
interface Todo {
id: string
name: string
complete: boolean
dueDate?: string | null
}
interface Props {
eventId: string
initialTodos: Todo[]
onUpdate: () => void
}
export default function ToDoList({ eventId, initialTodos, onUpdate }: Props) {
const [todos, setTodos] = useState(initialTodos)
const [newName, setNewName] = useState('')
const [newDueDate, setNewDueDate] = useState('')
async function handleAdd() {
if (!newName.trim()) return
const res = await fetch(`/api/events/${eventId}/todo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName, dueDate: newDueDate || null }),
})
const data = await res.json()
if (res.ok) {
setTodos(prev => [...prev, data])
setNewName('')
setNewDueDate('')
}
if (onUpdate) await onUpdate()
}
async function toggleComplete(id: string, complete: boolean) {
const res = await fetch(`/api/events/${eventId}/todo/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ complete }),
})
if (res.ok) {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, complete } : todo
)
)
}
if (onUpdate) await onUpdate()
}
async function handleDelete(id: string) {
const res = await fetch(`/api/events/${eventId}/todo/${id}`, {
method: 'DELETE',
})
if (res.ok) {
setTodos(prev => prev.filter(todo => todo.id !== id))
}
if (onUpdate) await onUpdate()
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">To Do List</h2>
<div className="flex gap-2">
<input
className="input input-bordered w-full"
placeholder="New To Do..."
value={newName}
onChange={e => setNewName(e.target.value)}
/>
<input
type="date"
className="input input-bordered"
value={newDueDate}
onChange={e => setNewDueDate(e.target.value)}
/>
<button className="btn btn-primary" onClick={handleAdd}>Add</button>
</div>
<ul className="space-y-2">
{todos.map(todo => (
<li
key={todo.id}
className="flex items-center justify-between p-3 bg-[#00000010] rounded-lg"
>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={todo.complete}
onChange={e => toggleComplete(todo.id, e.target.checked)}
/>
<span className={todo.complete ? 'line-through text-gray-400' : ''}>
{todo.name}
</span>
{todo.dueDate && (
<span className="text-sm text-gray-500 ml-2">
(Due {new Date(todo.dueDate).toLocaleDateString()})
</span>
)}
</div>
<button className="text-red-500 text-sm" onClick={() => handleDelete(todo.id)}>
Delete
</button>
</li>
))}
</ul>
</div>
)
}

5
lib/helper/fetchTodos.ts Normal file
View File

@@ -0,0 +1,5 @@
export async function fetchEventTodos(eventId: string) {
const res = await fetch(`/api/events/${eventId}/todo`)
if (!res.ok) throw new Error('Failed to fetch todos')
return await res.json()
}

View File

@@ -160,4 +160,37 @@ export const mutations = {
});
},
async addTodoToEvent(data: {
eventId: string;
name: string;
dueDate?: string;
}) {
return await prisma.eventTodo.create({
data: {
name: data.name,
eventId: data.eventId,
dueDate: data.dueDate ? new Date(data.dueDate) : undefined,
},
});
},
async updateEventTodo(id: string, data: Partial<{ name: string; dueDate?: string; complete: boolean }>) {
const { dueDate, ...rest } = data;
return await prisma.eventTodo.update({
where: { id },
data: {
...rest,
...(dueDate !== undefined ? { dueDate: new Date(dueDate) } : {}),
},
});
},
async deleteEventTodo(id: string) {
return await prisma.eventTodo.delete({
where: { id },
});
},
};

View File

@@ -71,6 +71,9 @@ export const queries = {
guestBookEntry: true,
},
},
todos: {
orderBy: { dueDate: 'asc' },
},
}
})
return event

View File

@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "EventTodo" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"complete" BOOLEAN NOT NULL DEFAULT false,
"dueDate" TIMESTAMP(3),
"eventId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EventTodo_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "EventTodo" ADD CONSTRAINT "EventTodo_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -34,6 +34,7 @@ model Event {
guests Guest[]
eventGuests EventGuest[]
notes String?
todos EventTodo[]
createdAt DateTime @default(now())
}
@@ -88,3 +89,17 @@ model EventGuest {
@@unique([eventId, guestBookEntryId])
}
model EventTodo {
id String @id @default(cuid())
name String
complete Boolean @default(false)
dueDate DateTime?
event Event @relation(fields: [eventId], references: [id])
eventId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Optional for future extensibility
// category String?
// assignedTo String? // could link to User in future
}