added bulk upload to guest book

This commit is contained in:
briannelson95
2025-07-02 17:22:16 -04:00
parent 81102fbae9
commit 1f8b0473e6
9 changed files with 211 additions and 5 deletions

View File

@@ -24,6 +24,10 @@ My goal for this project is to be an all-in-one self hosted event planner for ma
- [x] Add Guests to events
- [ ] Invite guests via email
- [ ] Create local account for guest
- [x] Bulk upload
- Creates GuestBookEntry
- Skips dupicates
- Updates entries if uploaded more than once
- [x] Managing RSVP lists
- [ ] Guest accounts
- [ ] Gift Registries
@@ -125,6 +129,48 @@ docker compose up --build
```
This will expose your instance at http://localhost:3000
### Advanced Features
#### Bulk Uploading Guests to the Guest Book
You can quickly populate your Guest Book by uploading a `.csv` file with your guest data. This is useful for importing lists from spreadsheets or other planning tools.
##### Required Format
The CSV file must contain the following column headers:
| Column Name | Required | Description |
|-------------|----------|-------------|
|`first` | ✅ Yes | First name of guest|
|`last` | ✅ Yes | Last name of guest|
|`side` | ✅ Yes | The side of the couple they are on (e.g., "Bride", "Groom")|
|`email` | ❌ No | Guest's email address|
|`phone` | ❌ No | Guest's phone number|
|`address` | ❌ No | Street address|
|`notes` | ❌ No | Any special notes or details|
> Only `first`, `last`, and `side` are required — the rest are optional.
###### Download Example CSV
To helo you get started, you can download a sample CSV file here:
[Download sample_guest_upload.csv](public/sample_guest_upload.csv)
This example includes 2 entries:
- John Smith (side: Groom)
- Jane Dow (side: Bride)
Feel free to use this as a template for formatting your own guest lsit before uploading.
##### Example CSV Content
```
first,last,side,email,phone,address,notes
Alice,Smith,Brian,alice@example.com,555-1234,"123 Main St","Vegetarian"
Bob,Jones,Kate,bob@example.com,,,
```
##### Duplicates
- Uploads skip exact duplicates (based on first and last name).
- If a match is found and new information is available (e.g. a phone number), the guest record will be updated automatically.
##### Uploading a File
1. Navigate to the **Guest Book** page.
2. Click the "**Upload CSV**" button next to the "**Add Guest**" button.
3. Select a `.csv` file from your computer.
4. Confirm the filename is displayed.
5. Click "**Upload**" — a confirmation will be shown with the number of guests added or updated.
## Built With
- NextJS 15
- NextAuth
@@ -134,3 +180,4 @@ This will expose your instance at http://localhost:3000
- Docker
### Ready to start planning your wedding or big event? Try it now or contribute ideas via [GitHub Issues](https://github.com/briannelson95/wedding-planner/issues).

View File

@@ -19,7 +19,7 @@ export default async function DashboardPage() {
<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'>
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
{events.map((item) => (
<EventInfoQuickView key={item.id} {...item} />
))}

View File

@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { parse } from 'csv-parse/sync'
export async function POST(req: NextRequest) {
try {
const formData = await req.formData()
const file = formData.get('file') as File
if (!file || file.type !== 'text/csv') {
return NextResponse.json({ message: "Invalid file type or missing CSV file."}, { status: 400 })
}
const buffer = Buffer.from(await file.arrayBuffer())
const csvString = buffer.toString('utf-8')
const records = parse(csvString, {
columns: true,
skip_empty_lines: true,
trim: true,
})
const validRecords = records.filter((r: any) => r.first && r.last && r.side)
let created = 0
let updated = 0
for (const r of validRecords) {
const existing = await prisma.guestBookEntry.findFirst({
where: {
fName: r.first,
lName: r.last,
}
})
const baseData = {
fName: r.first,
lName: r.last,
side: r.side,
email: r.email || undefined,
phone: r.phone || undefined,
address: r.address || undefined,
notes: r.notes || undefined,
}
if (existing) {
const updates: Record<string, string | undefined> = {}
if (!existing.email && r.email) updates.email = r.email
if (!existing.phone && r.phone) updates.phone = r.phone
if (!existing.address && r.address) updates.address = r.address
if (!existing.notes && r.notes) updates.notes = r.notes
if (!existing.side && r.side) updates.side = r.side
if (Object.keys(updates).length > 0) {
await prisma.guestBookEntry.update({
where: { id: existing.id },
data: updates,
})
updated++
}
} else {
await prisma.guestBookEntry.create({
data: baseData,
})
created++
}
}
return NextResponse.json({
success: true,
created,
updated,
totalProcessed: created + updated
})
} catch (err) {
console.error('[CSV Upload Error]', err)
return NextResponse.json({ message: 'Failed to upload guests' }, { status: 500 })
}
}

View File

@@ -0,0 +1,63 @@
'use client'
import React, { useState } from 'react'
export default function BulkUploadGuest() {
const [file, setFile] = useState<File | null>(null);
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false)
async function handleUpload(e: React.FormEvent) {
e.preventDefault()
if (!file) return
const formData = new FormData()
formData.append('file', file)
setStatus('Uploading...')
const res = await fetch('/api/guestbook/bulk-upload', {
method: 'POST',
body: formData,
})
const result = await res.json()
if (res.ok) {
setStatus(`Uploaded ${result.count} guests successfull`)
} else {
setStatus(`Upload failed: ${result.message}`)
}
}
return (
<div className="flex flex-col md:flex-row items-start md:items-center gap-2">
<label className="btn btn-outline btn-sm">
Select CSV
<input
type="file"
accept=".csv"
hidden
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
</label>
{file && (
<div className="text-sm truncate max-w-xs">
📄 {file.name}
</div>
)}
{file && (
<button
className="btn btn-primary btn-sm"
onClick={handleUpload}
disabled={loading}
>
{loading ? 'Uploading...' : 'Upload'}
</button>
)}
{status && (
<p className="text-sm text-gray-600">{status}</p>
)}
</div>
)
}

View File

@@ -5,6 +5,7 @@ import AddGuestBookEntryModal from '@/components/AddGuestBookEntryModal'
import GuestBookList from '@/components/GuestBookList'
import TableIcon from './icons/TableIcon'
import GuestCardIcon from './icons/GuestCardIcon'
import BulkUploadGuest from './BulkUploadGuest'
interface GuestBookEntry {
id: string
@@ -62,7 +63,10 @@ export default function GuestBookPageClient({ entries }: { entries: GuestBookEnt
</button>
</div>
</div>
<button onClick={() => setIsOpen(true)} className="btn btn-primary">Add Guest</button>
<div className='flex gap-2'>
<button onClick={() => setIsOpen(true)} className="btn btn-primary">Add Guest</button>
<BulkUploadGuest />
</div>
</div>
<GuestBookList view={view} entries={entries} />

View File

@@ -3,13 +3,13 @@ services:
image: postgres:15
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: wedding_planner
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./data/postgres:/var/lib/postgresql/data
volumes:
pgdata:

7
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@types/nodemailer": "^6.4.17",
"bcrypt": "^6.0.0",
"crypto": "^1.0.1",
"csv-parse": "^5.6.0",
"next": "15.3.4",
"next-auth": "^4.24.11",
"nodemailer": "^6.9.4",
@@ -2055,6 +2056,12 @@
"version": "3.1.3",
"license": "MIT"
},
"node_modules/csv-parse": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"dev": true,

View File

@@ -16,6 +16,7 @@
"@types/nodemailer": "^6.4.17",
"bcrypt": "^6.0.0",
"crypto": "^1.0.1",
"csv-parse": "^5.6.0",
"next": "15.3.4",
"next-auth": "^4.24.11",
"nodemailer": "^6.9.4",

View File

@@ -0,0 +1,3 @@
first,last,side,email,phone,address,notes
John,Smith,Groom,john@example.com,555-1234,123 Wedding Ln,Allergic to nuts
Jane,Doe,Bride,jane@example.com,555-5678,456 Celebration St,Vegan
1 first last side email phone address notes
2 John Smith Groom john@example.com 555-1234 123 Wedding Ln Allergic to nuts
3 Jane Doe Bride jane@example.com 555-5678 456 Celebration St Vegan