added bulk upload to guest book
This commit is contained in:
47
README.md
47
README.md
@@ -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).
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async function DashboardPage() {
|
||||
<CreateEventClient />
|
||||
</div>
|
||||
{!events.length && <>You don'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} />
|
||||
))}
|
||||
|
||||
81
app/api/guestbook/bulk-upload/route.ts
Normal file
81
app/api/guestbook/bulk-upload/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
63
components/BulkUploadGuest.tsx
Normal file
63
components/BulkUploadGuest.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
7
package-lock.json
generated
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
public/sample_guest_upload.csv
Normal file
3
public/sample_guest_upload.csv
Normal 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
|
||||
|
Reference in New Issue
Block a user