diff --git a/README.md b/README.md index e91ba95..a0746a7 100644 --- a/README.md +++ b/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). + diff --git a/app/(auth)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx index ce357df..fc31f49 100644 --- a/app/(auth)/dashboard/page.tsx +++ b/app/(auth)/dashboard/page.tsx @@ -19,7 +19,7 @@ export default async function DashboardPage() { {!events.length && <>You don't have any events yet. Create your first event.} -
+
{events.map((item) => ( ))} diff --git a/app/api/guestbook/bulk-upload/route.ts b/app/api/guestbook/bulk-upload/route.ts new file mode 100644 index 0000000..77eb56f --- /dev/null +++ b/app/api/guestbook/bulk-upload/route.ts @@ -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 = {} + + 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 }) + } +} \ No newline at end of file diff --git a/components/BulkUploadGuest.tsx b/components/BulkUploadGuest.tsx new file mode 100644 index 0000000..da84b76 --- /dev/null +++ b/components/BulkUploadGuest.tsx @@ -0,0 +1,63 @@ +'use client' +import React, { useState } from 'react' + +export default function BulkUploadGuest() { + const [file, setFile] = useState(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 ( +
+ + + {file && ( +
+ 📄 {file.name} +
+ )} + + {file && ( + + )} + + {status && ( +

{status}

+ )} +
+ ) +} diff --git a/components/GuestBookPageClient.tsx b/components/GuestBookPageClient.tsx index 46c9ba3..ecd5395 100644 --- a/components/GuestBookPageClient.tsx +++ b/components/GuestBookPageClient.tsx @@ -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
- +
+ + +
diff --git a/docker-compose.yml b/docker-compose.yml index 95a6e16..96dc538 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/package-lock.json b/package-lock.json index a384aab..ddd2b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index 9ea8161..1f0b4ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/sample_guest_upload.csv b/public/sample_guest_upload.csv new file mode 100644 index 0000000..3ee2128 --- /dev/null +++ b/public/sample_guest_upload.csv @@ -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