diff --git a/README.md b/README.md index 92464fa..e985cd2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ My goal for this project is to be an all-in-one self hosted event planner for ma - [x] First time setup to create the admin user - [x] Invite users via email (smtp) users can be COUPLE, PLANNER, GUEST - [x] Create local accounts (no use of SMTP) -- [ ] Creating custom events +- [x] Creating and Editing custom events - [ ] Information about each event - Date/Time - Event type @@ -40,6 +40,9 @@ My goal for this project is to be an all-in-one self hosted event planner for ma - added usernames to `Users` table - updated first time setup to include username creation +#### 6.25.25 +- now able to see and edit event data from the individual event page + ## 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 0a217c9..88728fb 100644 --- a/app/(auth)/dashboard/page.tsx +++ b/app/(auth)/dashboard/page.tsx @@ -18,8 +18,8 @@ export default async function DashboardPage() {

Name: {item.name}

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

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

-

Creator ID:{item.creatorId}

-

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

+

Created By: {item.creator.username}

+

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

diff --git a/app/(auth)/events/[eventId]/page.tsx b/app/(auth)/events/[eventId]/page.tsx index 182b294..9adb68e 100644 --- a/app/(auth)/events/[eventId]/page.tsx +++ b/app/(auth)/events/[eventId]/page.tsx @@ -1,15 +1,24 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import EventInfoDisplay from '@/components/EventInfoDisplay' +import HeadingWithEdit from '@/components/HeadingWithEdit' import { queries } from '@/lib/queries' import React from 'react' export default async function SingleEventPage({ params }: { params: { eventId: string }}) { - console.log(params) const data = await queries.singleEvent(params.eventId) + console.log(data) return (
-

- {data?.name} -

+ {data ? ( + // @ts-ignore + + ) : ( +

Event not found.

+ )} + {data?.name && ( + + )}
) } diff --git a/app/api/events/[eventId]/route.ts b/app/api/events/[eventId]/route.ts new file mode 100644 index 0000000..647dcb0 --- /dev/null +++ b/app/api/events/[eventId]/route.ts @@ -0,0 +1,22 @@ +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: { eventId: string } }) { + const session = await getServerSession(authOptions); + if (!session?.user) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + const eventId = params.eventId; + const body = await req.json(); + + try { + const updated = await mutations.updateEvent(eventId, body); + return NextResponse.json(updated); + } catch (error) { + console.error('[PATCH EVENT]', error); + return new NextResponse('Failed to update event', { status: 500 }); + } +} diff --git a/components/EventInfoDisplay.tsx b/components/EventInfoDisplay.tsx new file mode 100644 index 0000000..68cbce6 --- /dev/null +++ b/components/EventInfoDisplay.tsx @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +'use client' + +import React, { useState } from 'react' + +interface Creator { + id: string + email: string + name: string | null + role: 'COUPLE' | 'PLANNER' | 'GUEST' +} + +interface EventData { + id: string + name: string + date: Date | null + location: string | null + creatorId: string + createdAt: string + creator: Creator + guests: any[] +} + +interface Props { + event: EventData +} + +export default function EventInfoDisplay({ event }: Props) { + const [isEditing, setIsEditing] = useState(false) + + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + const [dateTime, setDateTime] = useState(() => { + if (event.date) { + const date = new Date(event.date); + return new Date(date.getTime() - date.getTimezoneOffset() * 60000) + .toISOString() + .slice(0, 16); // format: "yyyy-MM-ddTHH:mm" + } + return ''; + }); + const [form, setForm] = useState({ + name: event.name, + date: dateTime, + location: event.location || '', + }) + + function handleChange(e: React.ChangeEvent) { + const { name, value } = e.target + setForm(prev => ({ ...prev, [name]: value })) + } + + async function handleSave() { + setSaving(true) + setError('') + try { + const res = await fetch(`/api/events/${event.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }) + + if (!res.ok) { + const data = await res.json() + setError(data.message || 'Update failed') + return + } + + setIsEditing(false) + } catch (err) { + setError('Something went wrong.') + } finally { + setSaving(false) + } + } + + 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 && ( +
+ +
+ )} +
+ ) +} diff --git a/components/HeadingWithEdit.tsx b/components/HeadingWithEdit.tsx new file mode 100644 index 0000000..7f00e8f --- /dev/null +++ b/components/HeadingWithEdit.tsx @@ -0,0 +1,77 @@ +'use client' + +import React, { useState } from 'react' +import PencilIcon from './icons/PencilIcon' + +export default function HeadingWithEdit({ + title, + eventId, +}: { + title: string + eventId: string +}) { + const [edit, setEdit] = useState(false) + const [currentTitle, setCurrentTitle] = useState(title) + const [newTitle, setNewTitle] = useState(title) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + async function handleSave() { + setSaving(true) + setError('') + + try { + const res = await fetch(`/api/events/${eventId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: newTitle }), + }) + + if (!res.ok) { + const data = await res.json() + setError(data.message || 'Failed to update event') + return + } + + setCurrentTitle(newTitle) // update display title + setEdit(false) + } catch (err) { + console.error(err) + setError('Something went wrong') + } finally { + setSaving(false) + } + } + + return ( + <> +

+ {edit ? ( + setNewTitle(e.target.value)} + className="border rounded-lg px-2 py-1" + /> + ) : ( + currentTitle + )} + + {edit && ( + + )} +

+ {error &&

{error}

} + + ) +} diff --git a/components/icons/PencilIcon.tsx b/components/icons/PencilIcon.tsx new file mode 100644 index 0000000..02e81ac --- /dev/null +++ b/components/icons/PencilIcon.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +export default function PencilIcon() { + return ( + + + + + + ) +} diff --git a/lib/mutations.ts b/lib/mutations.ts index 3994602..197eb93 100644 --- a/lib/mutations.ts +++ b/lib/mutations.ts @@ -57,4 +57,31 @@ export const mutations = { return user }, + + async updateEvent( + eventId: string, + data: Partial<{ name: string; date: string; location: string }> + ) { + const { date, ...rest } = data; + + let parsedDate: Date | undefined = undefined; + + if (date) { + // Parse full datetime-local string into Date object + parsedDate = new Date(date); // Automatically handled as local time + } + + const event = await prisma.event.update({ + where: { id: eventId }, + data: { + ...rest, + ...(parsedDate ? { date: parsedDate } : {}), + }, + }); + + return event; + } + + + }; \ No newline at end of file diff --git a/lib/queries.ts b/lib/queries.ts index 270a517..5c22af9 100644 --- a/lib/queries.ts +++ b/lib/queries.ts @@ -2,7 +2,16 @@ import { prisma } from './prisma'; export const queries = { async fetchEvents() { - const allEvents = await prisma.event.findMany() + const allEvents = await prisma.event.findMany({ + include: { + creator: { + select: { + id: true, + username: true + } + } + } + }) console.log(allEvents) return allEvents; }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e1829bf..6ba0c56 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,14 +25,14 @@ enum Role { } model Event { - id String @id @default(cuid()) - name String - date DateTime? - location String? - creator User @relation("EventCreator", fields: [creatorId], references: [id]) - creatorId String - guests Guest[] - createdAt DateTime @default(now()) + id String @id @default(cuid()) + name String + date DateTime? + location String? + creator User @relation("EventCreator", fields: [creatorId], references: [id]) + creatorId String + guests Guest[] + createdAt DateTime @default(now()) } model Guest {