venues and ui changes

This commit is contained in:
2025-07-24 09:42:57 -04:00
parent 049def6886
commit 27590f9509
24 changed files with 757 additions and 164 deletions

View File

@@ -71,6 +71,23 @@ My goal for this project is to be an all-in-one self hosted event planner for ma
- Inline editing with live preview
- Task list per event with due dates & completion toggle
#### 7.24.25 Notes and Tasks
**Major Update**
- Added Venues
- Venues are significant locations which can be added to an event.
| Column | Default | Required | Type |
| --- | --- | --- | --- |
| Name | null | Yes | String |
| Address | null | Yes | String |
| City/Town | null | Yes | String |
| State | null | Yes | String |
| Postal/Area Code | null | Yes | String |
| Country | 'United States' | Yes | String |
| Phone | null | No | String |
| Email | null | No | String |
- UI changes to Dashboard to make use of Shadcn components
## Getting Started
This app is fully deployable with Docker or runnable in development with Node.

View File

@@ -1,55 +1,57 @@
import AddFirstGuestBookEntryClient from '@/components/AddFirstGuestBookEntryClient'
import CreateEventClient from '@/components/CreateEventClient'
import EventInfoQuickView from '@/components/EventInfoQuickView'
import GuestBookQuickView from '@/components/GuestBookQuickView'
import DashboardEvents from '@/components/dashboard/DashboardEvents'
import DashboardGuestBook from '@/components/dashboard/DashboardGuestBook'
import { queries } from '@/lib/queries'
import Link from 'next/link'
import React from 'react'
export default async function DashboardPage() {
const events = await queries.fetchEvents();
const events = await queries.fetchQuickViewEvents();
const guestBookData = await queries.fetchGuestBookEntries({ takeOnlyRecent: 5 });
const guestBookEntries = Array.isArray(guestBookData) ? guestBookData : guestBookData.entries;
return (
<div className='grid grid-cols-1 md:grid-cols-7 gap-4'>
<div className='md:col-span-5 md:row-span-3 bg-[#00000008] rounded-xl p-4 md:p-6 relative'>
<div>
<div className='w-full flex items-center justify-between'>
<h2 className='text-lg font-semibold py-4'>Your Events</h2>
<CreateEventClient />
<>
<div className='grid grid-cols-1 md:grid-cols-7 gap-4'>
<DashboardEvents events={events} />
<DashboardGuestBook guestBookEntries={guestBookEntries} />
{/* <div className='md:col-span-5 md:row-span-3 bg-[#00000008] rounded-xl p-4 md:p-6 relative'>
<div>
<div className='w-full flex items-center justify-between'>
<h2 className='text-lg font-semibold py-4'>Your Events</h2>
<CreateEventClient />
</div>
{!events.length && <>You don&apos;t have any events yet. Create your first event.</>}
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
{events.map((item) => (
<EventInfoQuickView key={item.id} {...item} />
))}
</div>
</div>
{!events.length && <>You don&apos;t have any events yet. Create your first event.</>}
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
{events.map((item) => (
<EventInfoQuickView key={item.id} {...item} />
<div className='w-full text-right mt-2'>
<Link href={'/events'} className='md:absolute bottom-4 right-4 text-sm text-brand-primary-400 hover:underline'>
View all
</Link>
</div>
</div>
<div className='md:row-span-5 md:col-start-6 col-span-2 bg-[#00000008] rounded-xl p-6'>
<div className='py-4 flex justify-between'>
<h2 className='text-lg font-semibold'>Guest Book</h2>
<Link
href={'/guest-book'}
className='hover:cursor-pointer hover:underline text-brand-primary-500'
>
View All
</Link>
</div>
<div className='space-y-2'>
{!guestBookEntries.length && <AddFirstGuestBookEntryClient />}
{guestBookEntries.map(entry => (
<GuestBookQuickView key={entry.id} {...entry} />
))}
</div>
</div>
<div className='w-full text-right mt-2'>
<Link href={'/events'} className='md:absolute bottom-4 right-4 text-sm text-brand-primary-400 hover:underline'>
View all
</Link>
</div>
</div> */}
</div>
<div className='md:row-span-5 md:col-start-6 col-span-2 bg-[#00000008] rounded-xl p-6'>
<div className='py-4 flex justify-between'>
<h2 className='text-lg font-semibold'>Guest Book</h2>
<Link
href={'/guest-book'}
className='hover:cursor-pointer hover:underline text-brand-primary-500'
>
View All
</Link>
</div>
<div className='space-y-2'>
{!guestBookEntries.length && <AddFirstGuestBookEntryClient />}
{guestBookEntries.map(entry => (
<GuestBookQuickView key={entry.id} {...entry} />
))}
</div>
</div>
</div>
</>
)
}

View File

@@ -1,13 +0,0 @@
import LocationsTable from '@/components/tables/LocationsTable'
import { queries } from '@/lib/queries'
import React from 'react'
export default async function LocationsPage() {
const eventLocations = await queries.fetchAllLocations()
return (
<div>
<LocationsTable eventLocations={eventLocations} />
</div>
)
}

View File

@@ -0,0 +1,13 @@
import VenuesTable from '@/components/tables/VenuesTable'
import { queries } from '@/lib/queries'
import React from 'react'
export default async function LocationsPage() {
const venues = await queries.fetchAllLocations()
return (
<div>
<VenuesTable eventLocations={venues} />
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export async function GET(
req: NextRequest,
{ params }: { params: { eventId: string } }
) {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
}
try {
const event = await prisma.event.findUnique({
where: { id: params.eventId },
include: {
creator: {
select: { id: true, name: true, email: true, role: true }
},
venue: true,
todos: {
orderBy: [
{ complete: 'asc' },
{ dueDate: 'asc' }
]
},
eventGuests: {
include: {
guestBookEntry: true
}
}
}
})
if (!event) {
return NextResponse.json({ message: 'Event not found' }, { status: 404 })
}
return NextResponse.json(event)
} catch (err) {
console.error(err)
return NextResponse.json({ message: 'Error fetching event' }, { status: 500 })
}
}

View File

@@ -1,27 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
import { mutations } from '@/lib/mutations';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../auth/[...nextauth]/route';
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export async function PATCH(req: NextRequest, { params }: { params: { eventId: string } }) {
const session = await getServerSession(authOptions);
const session = await getServerSession(authOptions)
if (!session?.user) {
return new NextResponse('Unauthorized', { status: 401 });
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
}
const eventId = params.eventId;
const body = await req.json();
const body = await req.json()
try {
const updated = await mutations.updateEvent(eventId, {
name: body.name,
date: body.date,
location: body.location,
notes: body.notes,
});
return NextResponse.json(updated);
} catch (error) {
console.error('[PATCH EVENT]', error);
return new NextResponse('Failed to update event', { status: 500 });
const updated = await prisma.event.update({
where: { id: params.eventId },
data: {
name: body.name,
date: body.date ? new Date(body.date) : undefined,
venueId: body.venueId || null,
},
})
return NextResponse.json(updated)
} catch (err) {
console.error(err)
return NextResponse.json({ message: 'Error updating event' }, { status: 500 })
}
}

View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
}
const body = await req.json()
try {
const venue = await prisma.venue.create({
data: {
name: body.name,
address: body.address,
city: body.city,
state: body.state,
postalCode: body.postalCode,
country: body.country,
phone: body.phone || undefined,
email: body.email || undefined
}
})
return NextResponse.json(venue)
} catch (err) {
console.error(err)
return NextResponse.json({ message: 'Error creating venue' }, { status: 500 })
}
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const venues = await prisma.venue.findMany()
return NextResponse.json(venues)
} catch (error) {
console.error('Failed to fetch venues:', error)
return new NextResponse('Failed to fetch venues', { status: 500 })
}
}

View File

@@ -1,15 +1,22 @@
import Link from 'next/link'
import React from 'react'
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card'
export default function EventInfoQuickView(props: EventProps) {
export default function EventInfoQuickView(props: QucikEventProps) {
return (
<Link href={`/events/${props.id}`} >
<div className='hover:cursor-pointer rounded-lg p-2 bg-brand-primary-900 hover:bg-brand-primary-800 transition-colors duration-200'>
<h3 className='text-md font-semibold'>{props.name}</h3>
<p>Date: {props.date ? props.date.toDateString() : 'null'}</p>
<p>Location: {props.location ? props.location.name : 'null'}</p>
<p className='text-xs mt-2'>Created By: {props.creator.username}</p>
</div>
<Link href={`/events/${props.id}`}>
<Card className='bg-brand-primary-900 hover:bg-brand-primary-800 transition-colors duration-200'>
<CardHeader>
<CardTitle>{props.name}</CardTitle>
</CardHeader>
<CardContent>
<p>Date: {props.date ? props.date.toDateString() : 'null'}</p>
<p>Location: {props.venue ? props.venue.name : 'null'}</p>
</CardContent>
<CardFooter>
<p className='text-xs mt-2'>Created By: {props.creator.username}</p>
</CardFooter>
</Card>
</Link>
)
}

View File

@@ -48,7 +48,7 @@ const data = {
},
{
title: "Locations",
url: "/locations",
url: "/venues",
icon: IconBuildingArch,
},
],

View File

@@ -0,0 +1,51 @@
import React from 'react'
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../ui/card'
import { Button } from '../ui/button'
import Link from 'next/link'
import EventInfoQuickView from '../EventInfoQuickView'
interface EventsProps {
events: {
id: string
name: string
date?: Date | null
creator: {
id: string,
username: string
},
venue?: {
name: string
} | null
}[]
}
export default function DashboardEvents({events}: EventsProps) {
return (
<Card className='md:col-span-5 pb-3'>
<CardHeader>
<div className='flex justify-between items-center'>
<CardTitle>
Your Events
</CardTitle>
<Button
className='bg-brand-primary-600 hover:bg-brand-primary-400'
>
Create Event
</Button>
</div>
</CardHeader>
<CardContent>
<div className='grid md:grid-cols-3'>
{events.map((item) => (
<EventInfoQuickView key={item.id} {...item} />
))}
</div>
</CardContent>
<CardFooter>
<div className='text-right w-full text-sm'>
<Link href={'/events'}>View all</Link>
</div>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
import Link from 'next/link'
import AddFirstGuestBookEntryClient from '../AddFirstGuestBookEntryClient'
import GuestBookQuickView from '../GuestBookQuickView'
interface GuestBookEntryProps {
guestBookEntries: {
id: string
fName: string
lName: string
email?: string | null
phone?: string | null
address?: string | null
notes?: string | null
side: string
congratulated?: boolean | null
createdAt: Date
}[]
}
export default function DashboardGuestBook(guestBookEntries: GuestBookEntryProps) {
return (
<Card className='md:col-start-6 col-span-2 row-span-2'>
<CardHeader>
<div className='flex justify-between items-center'>
<CardTitle>
Guest Book
</CardTitle>
<Link href={'/guest-book'}>View All</Link>
</div>
</CardHeader>
<CardContent className='space-y-2'>
{!guestBookEntries.guestBookEntries.length && <AddFirstGuestBookEntryClient />}
{guestBookEntries.guestBookEntries.map(entry => (
<GuestBookQuickView key={entry.id} {...entry} />
))}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,30 @@
'use client'
import React from 'react'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'
export default function DialogWrapper({
title,
description,
form,
open,
onOpenChange,
}: {
title: string,
description?: string,
form: React.ReactNode,
open: boolean,
onOpenChange: (open: boolean) => void
}) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-4">{form}</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -13,7 +13,7 @@ interface Props {
export default function EventDashboard({ event }: Props) {
const [todos, setTodos] = useState(event.todos)
async function refreshTodos() {
async function refreshTodos() {
try {
const data = await fetchEventTodos(event.id)
setTodos(data)
@@ -22,8 +22,6 @@ export default function EventDashboard({ event }: Props) {
}
}
console.log(todos)
return (
<div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
<EventInfo event={event} />

View File

@@ -4,6 +4,8 @@ import { Card, CardContent } from '../ui/card'
import { getDaysUntilEvent } from '@/lib/helper/getDaysUntilEvent'
import { Button } from '../ui/button'
import EventNotesEditor from '../EventNotesEditor'
import DialogWrapper from '../dialogs/DialogWrapper'
import EditEventForm from '../forms/EditEventForm'
interface EventProps {
event: EventData
@@ -11,6 +13,7 @@ interface EventProps {
export default function EventInfo({ event }: EventProps) {
const [daysLeft, setDaysLeft] = useState<number | null>(null)
const [isDialogOpen, setIsDialogOpen] = useState(false)
useEffect(() => {
if (event.date) {
@@ -19,6 +22,19 @@ export default function EventInfo({ event }: EventProps) {
}
}, [event.date])
async function refreshEventData(eventId: string) {
try {
const res = await fetch(`/api/events/${eventId}/fetch`)
if (!res.ok) throw new Error('Failed to fetch event data')
const data = await res.json()
return data
} catch (err) {
console.error('Failed to refresh event data:', err)
return null
}
}
return (
<div className='lg:col-span-1 space-y-4'>
<Card className='py-0'>
@@ -26,13 +42,18 @@ export default function EventInfo({ event }: EventProps) {
<h2 className='text-xl font-semibold'>Event Info</h2>
<p className='text-sm mt-2'>Name: {event.name}</p>
<p className='text-sm'>Date: {event.date ? event.date.toDateString() : 'Upcoming'}</p>
<p className='text-sm'>Location: {event.location ? event.location.name : 'No location yet'}</p>
<p className='text-sm'>Venue: {event.venue ? event.venue.name : 'No location yet'}</p>
{daysLeft !== null && (
<p className='text-sm mt-2 font-medium text-brand-primary-400'>
{daysLeft} days until this event!
</p>
)}
<Button className="mt-4 w-full bg-brand-primary-600 hover:bg-brand-primary-400">Edit Event</Button>
<Button
className="mt-4 w-full bg-brand-primary-600 hover:bg-brand-primary-400"
onClick={() => setIsDialogOpen(true)}
>
Edit Event
</Button>
</CardContent>
</Card>
<Card className='py-0'>
@@ -45,6 +66,20 @@ export default function EventInfo({ event }: EventProps) {
/>
</CardContent>
</Card>
<DialogWrapper
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
title="Edit Event"
description="Update the event details"
form={
<EditEventForm event={event} onSuccess={async () => {
await refreshEventData(event.id)
setIsDialogOpen(false)
}}
/>
}
/>
</div>
)
}

View File

@@ -0,0 +1,113 @@
'use client'
import React, { useState } from 'react'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { Label } from '../ui/label'
import { toast } from 'sonner'
interface CreateVenueFormProps {
onSuccess?: () => void
}
export default function CreateVenueForm({ onSuccess }: CreateVenueFormProps) {
const [formData, setFormData] = useState({
name: '',
address: '',
city: '',
state: '',
postalCode: '',
country: 'United States',
phone: '',
email: ''
})
const [loading, setLoading] = useState(false)
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!formData.name || !formData.address || !formData.city || !formData.state || !formData.postalCode) {
toast.error('Please fill in all required fields')
return
}
setLoading(true)
try {
const res = await fetch('/api/venues/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!res.ok) throw new Error('Failed to create venue')
toast.success('Venue created!')
setFormData({
name: '',
address: '',
city: '',
state: '',
postalCode: '',
country: 'United States',
phone: '',
email: ''
})
if (onSuccess) onSuccess()
} catch (err) {
console.error(err)
toast.error('Something went wrong')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{[
{ label: 'Name', name: 'name', required: true },
{ label: 'Address', name: 'address', required: true },
{ label: 'City', name: 'city', required: true },
{ label: 'State', name: 'state', required: true },
{ label: 'Postal Code', name: 'postalCode', required: true },
{ label: 'Country', name: 'country', required: false },
{ label: 'Phone', name: 'phone', required: false },
{ label: 'Email', name: 'email', required: false }
].map(field => (
<div key={field.name} className="space-y-1">
<Label htmlFor={field.name}>
{field.label}{field.required && ' *'}
</Label>
<Input
id={field.name}
name={field.name}
value={formData[field.name as keyof typeof formData]}
onChange={handleChange}
required={field.required}
/>
</div>
))}
{/* <div className="space-y-1">
<Label htmlFor='name'>
Name *
</Label>
<Input
id='name'
name='name'
value={formData[field.name as keyof typeof formData]}
onChange={handleChange}
required={field.required}
/>
</div> */}
<Button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Venue'}
</Button>
</form>
)
}

View File

@@ -0,0 +1,93 @@
'use client'
import React, { useState, useEffect } from 'react'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { Button } from '../ui/button'
import { toast } from 'sonner'
interface EditEventFormProps {
event: EventData
onSuccess?: () => void
}
export default function EditEventForm({ event, onSuccess }: EditEventFormProps) {
const [formData, setFormData] = useState({
name: event.name,
date: event.date?.toISOString().substring(0, 10) || '',
venueId: event.venue?.id || ''
})
const [venues, setVenues] = useState<{ id: string; name: string }[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
async function fetchVenues() {
const res = await fetch('/api/venues/fetch')
const data = await res.json()
setVenues(data)
}
fetchVenues()
}, [])
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
try {
const res = await fetch(`/api/events/${event.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!res.ok) throw new Error('Failed to update event')
toast.success('Event updated!')
if (onSuccess) onSuccess()
} catch (err) {
console.error(err)
toast.error('Something went wrong')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">Event Name</Label>
<Input id="name" name="name" value={formData.name} onChange={handleChange} required />
</div>
<div>
<Label htmlFor="date">Event Date</Label>
<Input id="date" name="date" type="date" value={formData.date} onChange={handleChange} />
</div>
<div>
<Label htmlFor="venueId">Venue</Label>
<select
id="venueId"
name="venueId"
className="input input-bordered w-full"
value={formData.venueId}
onChange={handleChange}
>
<option value="">No venue</option>
{venues.map(v => (
<option key={v.id} value={v.id}>
{v.name}
</option>
))}
</select>
</div>
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
</form>
)
}

View File

@@ -1,65 +0,0 @@
'use client'
import { ColumnDef } from '@tanstack/react-table'
import { DataTable } from './DataTable'
interface LocationRow {
id: string
name: string
address: string
city: string
state: string
postalCode: string
country: string
phone?: string | null
email?: string | null
}
interface Props {
eventLocations: LocationRow[]
}
const columns: ColumnDef<LocationRow>[] = [
{
accessorKey: 'name',
header: 'Name'
},
{
accessorKey: 'address',
header: 'Address'
},
{
accessorKey: 'city',
header: 'City'
},
{
accessorKey: 'state',
header: 'State'
},
{
accessorKey: 'postalCode',
header: 'Postal Code'
},
{
accessorKey: 'country',
header: 'Country'
},
{
accessorKey: 'phone',
header: 'Phone',
cell: ({ row }) => row.original.phone || '—'
},
{
accessorKey: 'email',
header: 'Email',
cell: ({ row }) => row.original.email || '—'
}
]
export default function LocationsTable({ eventLocations }: Props) {
return (
<div className="mt-4">
<DataTable columns={columns} data={eventLocations} />
</div>
)
}

View File

@@ -0,0 +1,99 @@
'use client'
import { ColumnDef } from '@tanstack/react-table'
import { DataTable } from './DataTable'
import { Button } from '../ui/button'
import DialogWrapper from '../dialogs/DialogWrapper'
import CreateVenueForm from '../forms/CreateVenueForm'
import { useState } from 'react'
import { fetchVenuesClient } from '@/lib/helper/fetchVenues'
interface LocationRow {
id: string
name: string
address: string
city: string
state: string
postalCode: string
country: string
phone?: string | null
email?: string | null
}
interface Props {
eventLocations: LocationRow[]
}
const columns: ColumnDef<LocationRow>[] = [
{
accessorKey: 'name',
header: 'Name'
},
{
accessorKey: 'address',
header: 'Address'
},
{
accessorKey: 'city',
header: 'City'
},
{
accessorKey: 'state',
header: 'State'
},
{
accessorKey: 'postalCode',
header: 'Postal Code'
},
{
accessorKey: 'country',
header: 'Country'
},
{
accessorKey: 'phone',
header: 'Phone',
cell: ({ row }) => row.original.phone || '—'
},
{
accessorKey: 'email',
header: 'Email',
cell: ({ row }) => row.original.email || '—'
}
]
export default function VenuesTable({ eventLocations }: Props) {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [venues, setVenues] = useState<LocationRow[]>(eventLocations)
async function refreshVenues() {
try {
const updated = await fetchVenuesClient()
setVenues(updated)
} catch (err) {
console.error('Failed to refresh venues:', err)
}
}
return (
<div className="space-y-4">
<Button
variant={'outline'}
onClick={() => setIsDialogOpen(true)}
>
Create Venue
</Button>
<DataTable columns={columns} data={venues} />
<DialogWrapper
title="Create a New Venue"
description="Enter the Venue information below"
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
form={<CreateVenueForm onSuccess={async () => {
await refreshVenues()
setIsDialogOpen(false)
}}
/>}
/>
</div>
)
}

View File

@@ -0,0 +1,5 @@
export async function fetchVenuesClient() {
const res = await fetch('/api/venues/fetch', { cache: 'no-store' }) // ensure no stale cache
if (!res.ok) throw new Error('Failed to fetch venues')
return res.json()
}

View File

@@ -10,13 +10,37 @@ export const queries = {
username: true
}
},
location: true
venue: true
},
})
return allEvents;
},
async fetchQuickViewEvents() {
const events = await prisma.event.findMany({
take: 3,
select: {
id: true,
name: true,
date: true,
creator: {
select: {
id: true,
username: true,
}
},
venue: {
select: {
name: true,
},
},
}
})
return events
},
async fetchEventGuests(eventId: string) {
return await prisma.eventGuest.findMany({
where: { eventId },
@@ -78,7 +102,7 @@ export const queries = {
{ dueDate: 'asc' },
],
},
location: true
venue: true
}
})
return event
@@ -136,7 +160,7 @@ export const queries = {
},
async fetchAllLocations() {
return await prisma.location.findMany()
return await prisma.venue.findMany()
},
}

View File

@@ -0,0 +1,34 @@
/*
Warnings:
- You are about to drop the column `locationid` on the `Event` table. All the data in the column will be lost.
- You are about to drop the `Location` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Event" DROP CONSTRAINT "Event_locationid_fkey";
-- AlterTable
ALTER TABLE "Event" DROP COLUMN "locationid",
ADD COLUMN "venueId" TEXT;
-- DropTable
DROP TABLE "Location";
-- CreateTable
CREATE TABLE "Venue" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"address" TEXT NOT NULL,
"city" TEXT NOT NULL,
"state" TEXT NOT NULL,
"postalCode" TEXT NOT NULL,
"country" TEXT NOT NULL DEFAULT 'United States',
"phone" TEXT,
"email" TEXT,
CONSTRAINT "Venue_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -30,8 +30,8 @@ model Event {
id String @id @default(cuid())
name String
date DateTime?
location Location? @relation(fields: [locationid], references: [id])
locationid String?
venue Venue? @relation(fields: [venueId], references: [id])
venueId String?
creator User @relation("EventCreator", fields: [creatorId], references: [id])
creatorId String
guests Guest[]
@@ -43,7 +43,7 @@ model Event {
FileUpload FileUpload[]
}
model Location {
model Venue {
id String @id @default(cuid())
name String
address String
@@ -53,17 +53,19 @@ model Location {
country String @default("United States")
phone String?
email String?
createdAt DateTime @default(now())
Event Event[]
}
model Guest {
id String @id @default(cuid())
event Event @relation(fields: [eventId], references: [id])
eventId String
name String
email String?
rsvp RsvpStatus @default(PENDING)
id String @id @default(cuid())
event Event @relation(fields: [eventId], references: [id])
eventId String
name String
email String?
rsvp RsvpStatus @default(PENDING)
// attended RsvpStatus @default(PENDING)
}
enum RsvpStatus {

17
types.d.ts vendored
View File

@@ -22,6 +22,19 @@ interface EventProps {
key: string;
}
interface QucikEventProps {
id: string
name: string
date?: Date | null
creator: {
id: string
username: string
},
venue?: {
name: string
} | null
}
interface Creator {
id: string
email: string
@@ -44,7 +57,7 @@ interface EventData {
id: string
name: string
date: Date | null
location: EventLocation | null
venue: Venue | null
creatorId: string
createdAt: string
creator: Creator
@@ -73,7 +86,7 @@ type User = {
role: 'COUPLE' | 'PLANNER' | 'GUEST'
}
interface EventLocation {
interface Venue {
id: string
name: string
address: string