diff --git a/backend/src/index.ts b/backend/src/index.ts
index 84e5ff1..96c2516 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -440,6 +440,34 @@ app.post('/api/events/:slug/rsvp', async (req: Request, res: Response) => {
}
});
+// Get RSVP by edit ID
+app.get('/api/rsvps/edit/:editId', async (req: Request, res: Response) => {
+ try {
+ const { editId } = req.params;
+ const rsvp = await db.get('SELECT * FROM rsvps WHERE edit_id = ?', [editId]);
+
+ if (!rsvp) {
+ return res.status(404).json({ error: 'RSVP not found' });
+ }
+
+ // Parse arrays for response
+ try {
+ rsvp.items_bringing = rsvp.items_bringing ? JSON.parse(rsvp.items_bringing) : [];
+ rsvp.guest_names = rsvp.guest_names ? JSON.parse(rsvp.guest_names) : [];
+ } catch (e) {
+ console.error('Error parsing arrays in response:', e);
+ rsvp.items_bringing = [];
+ rsvp.guest_names = [];
+ }
+
+ res.json(rsvp);
+ } catch (error) {
+ console.error('Error fetching RSVP by edit ID:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+
app.delete('/api/events/:slug/rsvps/:id', async (req: Request, res: Response) => {
try {
const { slug, id } = req.params;
@@ -460,6 +488,83 @@ app.delete('/api/events/:slug/rsvps/:id', async (req: Request, res: Response) =>
}
});
+// Update RSVP by edit ID
+app.put('/api/rsvps/edit/:editId', async (req: Request, res: Response) => {
+ try {
+ const { editId } = req.params;
+ const { name, attending, bringing_guests, guest_count, guest_names, items_bringing, other_items } = req.body;
+
+ // Find the RSVP by edit_id
+ const rsvp = await db.get('SELECT id, event_id FROM rsvps WHERE edit_id = ?', [editId]);
+
+ if (!rsvp) {
+ return res.status(404).json({ error: 'RSVP not found' });
+ }
+
+ const rsvpId = rsvp.id;
+ const eventId = rsvp.event_id;
+
+ // Parse items_bringing if it's a string
+ let parsedItemsBringing: string[] = [];
+ try {
+ if (typeof items_bringing === 'string') {
+ parsedItemsBringing = JSON.parse(items_bringing);
+ } else if (Array.isArray(items_bringing)) {
+ parsedItemsBringing = items_bringing;
+ }
+ } catch (e) {
+ console.error('Error parsing items_bringing:', e);
+ }
+
+ // Parse guest_names if it's a string
+ let parsedGuestNames: string[] = [];
+ try {
+ if (typeof guest_names === 'string' && guest_names.includes('[')) {
+ // If it's a JSON string array
+ parsedGuestNames = JSON.parse(guest_names);
+ } else if (typeof guest_names === 'string') {
+ // If it's a comma-separated string
+ parsedGuestNames = guest_names.split(',').map(name => name.trim()).filter(name => name);
+ } else if (Array.isArray(guest_names)) {
+ // If it's already an array
+ parsedGuestNames = guest_names.filter(name => name && name.trim());
+ }
+ } catch (e) {
+ console.error('Error parsing guest_names:', e);
+ parsedGuestNames = [];
+ }
+
+ // Update the RSVP
+ await db.run(
+ 'UPDATE rsvps SET name = ?, attending = ?, bringing_guests = ?, guest_count = ?, guest_names = ?, items_bringing = ?, other_items = ? WHERE id = ?',
+ [name, attending, bringing_guests, guest_count, JSON.stringify(parsedGuestNames), JSON.stringify(parsedItemsBringing), other_items || '', rsvpId]
+ );
+
+ // Get the updated RSVP to verify and return
+ const updatedRsvp = await db.get('SELECT * FROM rsvps WHERE id = ?', [rsvpId]);
+
+ if (!updatedRsvp) {
+ return res.status(404).json({ error: 'RSVP not found after update' });
+ }
+
+ // Parse arrays for response
+ try {
+ updatedRsvp.items_bringing = updatedRsvp.items_bringing ? JSON.parse(updatedRsvp.items_bringing) : [];
+ updatedRsvp.guest_names = updatedRsvp.guest_names ? JSON.parse(updatedRsvp.guest_names) : [];
+ } catch (e) {
+ console.error('Error parsing arrays in response:', e);
+ updatedRsvp.items_bringing = [];
+ updatedRsvp.guest_names = [];
+ }
+
+ res.json(updatedRsvp);
+ } catch (error) {
+ console.error('Error updating RSVP by edit ID:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+
// Update RSVP
app.put('/api/events/:slug/rsvps/:id', async (req: Request, res: Response) => {
try {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f7ba567..2aa3115 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -8,6 +8,7 @@ import EventForm from './components/EventForm';
import RSVPForm from './components/RSVPForm';
import EventAdmin from './components/EventAdmin';
import EventView from './components/EventView';
+import RSVPEditForm from './components/RSVPEditForm'; // Import the new component
import './App.css';
const darkTheme = createTheme({
@@ -71,6 +72,7 @@ const App: React.FC = () => {
} />
} />
} />
+ } /> {/* Add the new route */}
@@ -79,4 +81,4 @@ const App: React.FC = () => {
);
};
-export default App;
\ No newline at end of file
+export default App;
diff --git a/frontend/src/components/RSVPEditForm.tsx b/frontend/src/components/RSVPEditForm.tsx
new file mode 100644
index 0000000..87bd570
--- /dev/null
+++ b/frontend/src/components/RSVPEditForm.tsx
@@ -0,0 +1,558 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import {
+ Box,
+ Paper,
+ Typography,
+ TextField,
+ Button,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ SelectChangeEvent,
+ Container,
+ Checkbox,
+ ListItemText,
+ OutlinedInput,
+ Chip,
+} from '@mui/material';
+import { Event } from '../types';
+
+interface RSVPFormData {
+ name: string;
+ attending: string;
+ bringing_guests: string;
+ guest_count: number;
+ guest_names: string[];
+ items_bringing: string[];
+ other_items: string;
+}
+
+const RSVPEditForm: React.FC = () => {
+ const { slug, editId } = useParams<{ slug: string; editId: string }>();
+ const [formData, setFormData] = useState({
+ name: '',
+ attending: '',
+ bringing_guests: '',
+ guest_count: 1,
+ guest_names: [],
+ items_bringing: [],
+ other_items: ''
+ });
+ const [neededItems, setNeededItems] = useState([]);
+ const [claimedItems, setClaimedItems] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+ const navigate = useNavigate();
+ const [event, setEvent] = useState(null);
+ const [rsvpId, setRsvpId] = useState(null);
+
+ useEffect(() => {
+ const fetchRsvpDetails = async () => {
+ try {
+ const [eventResponse, rsvpResponse, rsvpsResponse] = await Promise.all([
+ axios.get(`/api/events/${slug}`),
+ axios.get(`/api/rsvps/edit/${editId}`), // New endpoint to fetch by editId
+ axios.get(`/api/events/${slug}/rsvps`) // To get all RSVPs for claimed items
+ ]);
+
+ if (!eventResponse.data || !rsvpResponse.data || !rsvpsResponse.data) {
+ throw new Error('Failed to fetch data from server');
+ }
+
+ setEvent(eventResponse.data);
+ setRsvpId(rsvpResponse.data.id);
+
+ // Pre-fill the form with existing RSVP data
+ setFormData({
+ name: rsvpResponse.data.name,
+ attending: rsvpResponse.data.attending,
+ bringing_guests: rsvpResponse.data.bringing_guests,
+ guest_count: rsvpResponse.data.guest_count,
+ guest_names: Array.isArray(rsvpResponse.data.guest_names) ? rsvpResponse.data.guest_names : (typeof rsvpResponse.data.guest_names === 'string' && rsvpResponse.data.guest_names ? JSON.parse(rsvpResponse.data.guest_names) : []),
+ items_bringing: Array.isArray(rsvpResponse.data.items_bringing) ? rsvpResponse.data.items_bringing : (typeof rsvpResponse.data.items_bringing === 'string' && rsvpResponse.data.items_bringing ? JSON.parse(rsvpResponse.data.items_bringing) : []),
+ other_items: rsvpResponse.data.other_items || ''
+ });
+
+ // Process needed items (same logic as RSVPForm)
+ let items: string[] = [];
+ if (eventResponse.data.needed_items) {
+ items = Array.isArray(eventResponse.data.needed_items)
+ ? eventResponse.data.needed_items
+ : typeof eventResponse.data.needed_items === 'string'
+ ? JSON.parse(eventResponse.data.needed_items)
+ : [];
+ }
+
+ // Get all claimed items from existing RSVPs (excluding the current one)
+ const claimed = new Set();
+ if (Array.isArray(rsvpsResponse.data)) {
+ rsvpsResponse.data.forEach((rsvp: any) => {
+ if (rsvp.id !== rsvpResponse.data.id) { // Exclude current RSVP's claimed items initially
+ try {
+ let rsvpItems: string[] = [];
+ if (typeof rsvp.items_bringing === 'string') {
+ try {
+ const parsed = JSON.parse(rsvp.items_bringing);
+ rsvpItems = Array.isArray(parsed) ? parsed : [];
+ } catch (e) {
+ console.error('Error parsing items_bringing JSON:', e);
+ }
+ } else if (Array.isArray(rsvp.items_bringing)) {
+ rsvpItems = rsvp.items_bringing;
+ }
+
+ if (Array.isArray(rsvpItems)) {
+ rsvpItems.forEach((item: string) => claimed.add(item));
+ }
+ } catch (e) {
+ console.error('Error processing RSVP items:', e);
+ }
+ }
+ });
+ }
+
+ // Filter out claimed items from available items, but include items the current RSVP already claimed
+ const availableItems = items.filter(item => !claimed.has(item) || (Array.isArray(rsvpResponse.data.items_bringing) ? rsvpResponse.data.items_bringing.includes(item) : (typeof rsvpResponse.data.items_bringing === 'string' && rsvpResponse.data.items_bringing ? JSON.parse(rsvpResponse.data.items_bringing).includes(item) : false)));
+
+ setNeededItems(availableItems);
+ setClaimedItems(Array.from(claimed)); // This will be claimed by others
+ setLoading(false);
+
+ } catch (error) {
+ console.error('Error fetching RSVP details:', error);
+ setError('Failed to load RSVP details. The link may be invalid or expired.');
+ setLoading(false);
+ }
+ };
+ fetchRsvpDetails();
+ }, [slug, editId]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+
+ if (name === 'attending') {
+ if (value === 'no' || value === 'maybe') {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value,
+ bringing_guests: 'no',
+ guest_count: 0,
+ guest_names: [],
+ items_bringing: [],
+ other_items: ''
+ }));
+ return;
+ }
+ }
+
+ if (name === 'bringing_guests') {
+ if (value === 'no') {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value,
+ guest_count: 0,
+ guest_names: []
+ }));
+ return;
+ } else if (value === 'yes') {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value,
+ guest_count: 1,
+ guest_names: ['']
+ }));
+ return;
+ }
+ }
+
+ if (name === 'guest_count') {
+ const count = parseInt(value) || 0;
+ setFormData(prev => ({
+ ...prev,
+ [name]: count,
+ guest_names: Array(count).fill('').map((_, i) => prev.guest_names[i] || '')
+ }));
+ return;
+ }
+
+ if (name.startsWith('guest_name_')) {
+ const index = parseInt(name.split('_')[2]);
+ setFormData(prev => ({
+ ...prev,
+ guest_names: prev.guest_names.map((name, i) => i === index ? value : name)
+ }));
+ return;
+ }
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSelectChange = (e: SelectChangeEvent) => {
+ const { name, value } = e.target;
+
+ if (name === 'attending' && value !== 'yes') {
+ setFormData(prev => ({
+ ...prev,
+ attending: value as 'no' | 'maybe',
+ bringing_guests: 'no',
+ guest_count: 0,
+ guest_names: [],
+ items_bringing: [],
+ other_items: ''
+ }));
+ } else if (name === 'bringing_guests') {
+ setFormData(prev => {
+ const maxGuests = event?.max_guests_per_rsvp;
+ let initialGuestCount = 1;
+
+ if (maxGuests === 0 && value === 'yes') {
+ return {
+ ...prev,
+ bringing_guests: 'no',
+ guest_count: 0,
+ guest_names: []
+ };
+ }
+
+ if (maxGuests !== undefined && maxGuests !== -1 && maxGuests < initialGuestCount) {
+ initialGuestCount = maxGuests;
+ }
+
+ return {
+ ...prev,
+ bringing_guests: value as 'yes' | 'no',
+ guest_count: value === 'yes' ? initialGuestCount : 0,
+ guest_names: value === 'no' ? [] : Array(initialGuestCount).fill('')
+ };
+ });
+ } else {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ }
+ };
+
+ const handleItemsChange = (e: SelectChangeEvent) => {
+ const { value } = e.target;
+ const itemsArray = Array.isArray(value) ? value : [];
+ setFormData(prev => ({
+ ...prev,
+ items_bringing: itemsArray
+ }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ setError(null);
+
+ if (!formData.name.trim() || !formData.attending) {
+ setError('Please fill in all required fields');
+ setIsSubmitting(false);
+ return;
+ }
+
+ if (formData.attending === 'yes' && !formData.bringing_guests) {
+ setError('Please indicate if you are bringing guests');
+ setIsSubmitting(false);
+ return;
+ }
+
+ if (formData.bringing_guests === 'yes' &&
+ (formData.guest_count < 1 ||
+ formData.guest_names.some(name => !name.trim()))) {
+ setError('Please provide names for all guests');
+ setIsSubmitting(false);
+ return;
+ }
+
+ try {
+ const splitOtherItems = formData.other_items
+ .split(/\r?\n|,/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .join(', ');
+ const submissionData = {
+ ...formData,
+ items_bringing: formData.items_bringing,
+ other_items: splitOtherItems
+ };
+
+ // Use the new PUT endpoint for updating by editId
+ await axios.put(`/api/rsvps/edit/${editId}`, submissionData);
+
+ setSuccess(true);
+ } catch (err) {
+ console.error('Error updating RSVP:', err);
+ setError('Failed to update RSVP. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (error || !event) {
+ return (
+
+ {error || 'Event or RSVP not found'}
+
+ );
+ }
+
+ if (success) {
+ return (
+
+
+
+
+
+ Success!
+
+
+ Your RSVP has been updated successfully.
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Edit Your RSVP
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ Are you attending?
+
+
+
+ {formData.attending === 'yes' && (
+ <>
+
+ Are you bringing any guests?
+
+
+
+ {formData.bringing_guests === 'yes' && (
+ <>
+ {
+ const value = parseInt(e.target.value);
+ if (isNaN(value)) return;
+
+ const maxGuests = event?.max_guests_per_rsvp;
+ let newCount = value;
+
+ if (maxGuests !== undefined && maxGuests !== -1 && value > maxGuests) {
+ newCount = maxGuests;
+ }
+
+ if (newCount < 1) newCount = 1;
+
+ setFormData(prev => ({
+ ...prev,
+ guest_count: newCount,
+ guest_names: Array(newCount).fill('').map((_, i) => prev.guest_names[i] || '')
+ }));
+ }}
+ fullWidth
+ variant="outlined"
+ required
+ inputProps={{
+ min: 1,
+ max: event?.max_guests_per_rsvp === -1 ? undefined : event?.max_guests_per_rsvp
+ }}
+ error={formData.guest_count < 1}
+ helperText={
+ formData.guest_count < 1
+ ? "Number of guests must be at least 1"
+ : event?.max_guests_per_rsvp === 0
+ ? "No additional guests allowed for this event"
+ : event?.max_guests_per_rsvp === -1
+ ? "No limit on number of guests"
+ : `Maximum ${event?.max_guests_per_rsvp} additional guests allowed`
+ }
+ />
+
+ {Array.from({ length: formData.guest_count }).map((_, index) => (
+
+ ))}
+ >
+ )}
+
+ {neededItems.length > 0 && (
+
+ What items are you bringing?
+ }
+ renderValue={(selected) => (
+
+ {selected.map((value) => (
+
+ ))}
+
+ )}
+ >
+ {neededItems.map((item) => (
+
+ ))}
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default RSVPEditForm;