editing event data
This commit is contained in:
@@ -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] First time setup to create the admin user
|
||||||
- [x] Invite users via email (smtp) users can be COUPLE, PLANNER, GUEST
|
- [x] Invite users via email (smtp) users can be COUPLE, PLANNER, GUEST
|
||||||
- [x] Create local accounts (no use of SMTP)
|
- [x] Create local accounts (no use of SMTP)
|
||||||
- [ ] Creating custom events
|
- [x] Creating and Editing custom events
|
||||||
- [ ] Information about each event
|
- [ ] Information about each event
|
||||||
- Date/Time
|
- Date/Time
|
||||||
- Event type
|
- 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
|
- added usernames to `Users` table
|
||||||
- updated first time setup to include username creation
|
- 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
|
## 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.
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export default async function DashboardPage() {
|
|||||||
<p>Name: <strong>{item.name}</strong></p>
|
<p>Name: <strong>{item.name}</strong></p>
|
||||||
<p>Date: {item.date ? item.date.toISOString() : 'null'}</p>
|
<p>Date: {item.date ? item.date.toISOString() : 'null'}</p>
|
||||||
<p>Location: {item.location ? item.location : 'null'}</p>
|
<p>Location: {item.location ? item.location : 'null'}</p>
|
||||||
<p>Creator ID:{item.creatorId}</p>
|
<p>Created By: {item.creator.username}</p>
|
||||||
<p>Created At:{item.createdAt.toISOString()}</p>
|
<p>Created At: {item.createdAt.toISOString()}</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -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 { queries } from '@/lib/queries'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default async function SingleEventPage({ params }: { params: { eventId: string }}) {
|
export default async function SingleEventPage({ params }: { params: { eventId: string }}) {
|
||||||
console.log(params)
|
|
||||||
const data = await queries.singleEvent(params.eventId)
|
const data = await queries.singleEvent(params.eventId)
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-4xl font-bold'>
|
{data ? (
|
||||||
{data?.name}
|
// @ts-ignore
|
||||||
</h1>
|
<EventInfoDisplay event={data} />
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-gray-500 mt-10">Event not found.</p>
|
||||||
|
)}
|
||||||
|
{data?.name && (
|
||||||
|
<HeadingWithEdit title={data?.name} eventId={data.id} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/api/events/[eventId]/route.ts
Normal file
22
app/api/events/[eventId]/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
172
components/EventInfoDisplay.tsx
Normal file
172
components/EventInfoDisplay.tsx
Normal file
@@ -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<HTMLInputElement>) {
|
||||||
|
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 (
|
||||||
|
<div className="border p-6 rounded-lg shadow bg-white space-y-4">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<h2 className="text-2xl font-bold">Event Info</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(prev => !prev)}
|
||||||
|
className="text-sm text-blue-600 underline"
|
||||||
|
>
|
||||||
|
{isEditing ? 'Cancel' : 'Edit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Event Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold">Event Name</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p>{event.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Event Date */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold">Date</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={dateTime}
|
||||||
|
onChange={(e) => setDateTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p>{event.date ? formatDate(event.date.toDateString()) : 'N/A'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold">Location</label>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
name="location"
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={form.location}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p>{event.location || 'N/A'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Creator Email */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold">Creator Email</label>
|
||||||
|
<p>{event.creator.email}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Created At */}
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-semibold">Created At</label>
|
||||||
|
<p>{formatDate(event.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="text-right">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
components/HeadingWithEdit.tsx
Normal file
77
components/HeadingWithEdit.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<h1 className="text-4xl font-bold flex gap-2 items-center">
|
||||||
|
{edit ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newTitle}
|
||||||
|
onChange={(e) => setNewTitle(e.target.value)}
|
||||||
|
className="border rounded-lg px-2 py-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
currentTitle
|
||||||
|
)}
|
||||||
|
<button onClick={() => setEdit(!edit)} className="hover:cursor-pointer">
|
||||||
|
<PencilIcon />
|
||||||
|
</button>
|
||||||
|
{edit && (
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="text-sm bg-blue-600 text-white rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
components/icons/PencilIcon.tsx
Normal file
11
components/icons/PencilIcon.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function PencilIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
|
||||||
|
<path d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32l8.4-8.4Z" />
|
||||||
|
<path d="M5.25 5.25a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3V13.5a.75.75 0 0 0-1.5 0v5.25a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5V8.25a1.5 1.5 0 0 1 1.5-1.5h5.25a.75.75 0 0 0 0-1.5H5.25Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,4 +57,31 @@ export const mutations = {
|
|||||||
|
|
||||||
return user
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,16 @@ import { prisma } from './prisma';
|
|||||||
|
|
||||||
export const queries = {
|
export const queries = {
|
||||||
async fetchEvents() {
|
async fetchEvents() {
|
||||||
const allEvents = await prisma.event.findMany()
|
const allEvents = await prisma.event.findMany({
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
console.log(allEvents)
|
console.log(allEvents)
|
||||||
return allEvents;
|
return allEvents;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ enum Role {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Event {
|
model Event {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
date DateTime?
|
date DateTime?
|
||||||
location String?
|
location String?
|
||||||
creator User @relation("EventCreator", fields: [creatorId], references: [id])
|
creator User @relation("EventCreator", fields: [creatorId], references: [id])
|
||||||
creatorId String
|
creatorId String
|
||||||
guests Guest[]
|
guests Guest[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Guest {
|
model Guest {
|
||||||
|
|||||||
Reference in New Issue
Block a user