Add view-only RSVPs page and View RSVPs button

This commit is contained in:
2025-04-30 14:26:04 -04:00
parent 47af7f5d88
commit ffcf959992
3 changed files with 281 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import EventList from './components/EventList';
import EventForm from './components/EventForm'; import EventForm from './components/EventForm';
import RSVPForm from './components/RSVPForm'; import RSVPForm from './components/RSVPForm';
import EventAdmin from './components/EventAdmin'; import EventAdmin from './components/EventAdmin';
import EventView from './components/EventView';
import './App.css'; import './App.css';
const darkTheme = createTheme({ const darkTheme = createTheme({
@@ -72,6 +73,7 @@ const App: React.FC = () => {
<Route path="/create" element={<EventForm />} /> <Route path="/create" element={<EventForm />} />
<Route path="/events/:slug/rsvp" element={<RSVPForm />} /> <Route path="/events/:slug/rsvp" element={<RSVPForm />} />
<Route path="/events/:slug/admin" element={<EventAdmin />} /> <Route path="/events/:slug/admin" element={<EventAdmin />} />
<Route path="/events/:slug/view" element={<EventView />} />
</Routes> </Routes>
</Container> </Container>
</Box> </Box>

View File

@@ -86,6 +86,18 @@ const EventList: React.FC = () => {
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions> <CardActions>
<Button
onClick={(e) => {
e.stopPropagation();
navigate(`/events/${event.slug}/view`);
}}
color="primary"
aria-label="view rsvps"
variant="text"
sx={{ ml: 1 }}
>
View RSVPs
</Button>
<Button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@@ -0,0 +1,267 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Paper,
Typography,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Container,
Chip,
} from '@mui/material';
import axios from 'axios';
interface RSVP {
id: number;
name: string;
attending: string;
bringing_guests: string;
guest_count: number;
guest_names: string;
items_bringing: string[] | string;
}
interface Event {
id: number;
title: string;
description: string;
date: string;
location: string;
slug: string;
needed_items?: string[] | string;
}
const EventView: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const [event, setEvent] = useState<Event | null>(null);
const [rsvps, setRsvps] = useState<RSVP[]>([]);
const [neededItems, setNeededItems] = useState<string[]>([]);
const [claimedItems, setClaimedItems] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchEventAndRsvps();
}, [slug]);
const fetchEventAndRsvps = async () => {
try {
const [eventResponse, rsvpsResponse] = await Promise.all([
axios.get(`/api/events/${slug}`),
axios.get(`/api/events/${slug}/rsvps`)
]);
setEvent(eventResponse.data);
// Process needed items
let items: string[] = [];
if (eventResponse.data.needed_items) {
try {
if (typeof eventResponse.data.needed_items === 'string') {
const parsed = JSON.parse(eventResponse.data.needed_items);
items = Array.isArray(parsed) ? parsed : [];
} else if (Array.isArray(eventResponse.data.needed_items)) {
items = eventResponse.data.needed_items;
}
} catch (e) {
console.error('Error parsing needed_items:', e);
items = [];
}
}
// Get all claimed items from existing RSVPs
const claimed = new Set<string>();
const processedRsvps = rsvpsResponse.data.map((rsvp: RSVP) => {
let itemsBringing: string[] = [];
try {
if (typeof rsvp.items_bringing === 'string') {
try {
const parsed = JSON.parse(rsvp.items_bringing);
itemsBringing = Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('Error parsing items_bringing JSON:', e);
}
} else if (Array.isArray(rsvp.items_bringing)) {
itemsBringing = rsvp.items_bringing;
}
if (itemsBringing.length > 0) {
itemsBringing.forEach(item => claimed.add(item));
}
} catch (e) {
console.error('Error processing items for RSVP:', e);
}
return {
...rsvp,
items_bringing: itemsBringing
};
});
// Filter out claimed items from needed items
const availableItems = items.filter(item => !claimed.has(item));
setNeededItems(availableItems);
setClaimedItems(Array.from(claimed));
setRsvps(processedRsvps);
setLoading(false);
} catch (error) {
setError('Failed to load event data');
setLoading(false);
}
};
if (loading) {
return (
<Container maxWidth="lg">
<Typography>Loading...</Typography>
</Container>
);
}
if (error || !event) {
return (
<Container maxWidth="lg">
<Typography color="error">{error || 'Event not found'}</Typography>
</Container>
);
}
return (
<Container maxWidth="lg">
<Paper elevation={3} sx={{ p: 4, mt: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Typography variant="h4" component="h2" color="primary">
{event.title} - RSVPs
</Typography>
<Button
variant="outlined"
onClick={() => navigate('/')}
>
Back to Events
</Button>
</Box>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" gutterBottom>
Items Status
</Typography>
<Box sx={{ display: 'flex', gap: 4 }}>
<Box>
<Typography variant="subtitle1" gutterBottom>
Still Needed:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{neededItems.map((item: string, index: number) => (
<Chip
key={`${item}-${index}`}
label={item}
color="primary"
variant="outlined"
/>
))}
{neededItems.length === 0 && (
<Typography variant="body2" color="text.secondary">
All items have been claimed
</Typography>
)}
</Box>
</Box>
<Box>
<Typography variant="subtitle1" gutterBottom>
Claimed Items:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{claimedItems.map((item: string, index: number) => (
<Chip
key={`${item}-${index}`}
label={item}
color="success"
/>
))}
{claimedItems.length === 0 && (
<Typography variant="body2" color="text.secondary">
No items have been claimed yet
</Typography>
)}
</Box>
</Box>
</Box>
</Box>
<Typography variant="h6" gutterBottom>
RSVPs ({rsvps.length})
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Attending</TableCell>
<TableCell>Guests</TableCell>
<TableCell>Items Bringing</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rsvps.map((rsvp: RSVP) => (
<TableRow key={rsvp.id}>
<TableCell>{rsvp.name}</TableCell>
<TableCell>{rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1)}</TableCell>
<TableCell>
{rsvp.bringing_guests === 'yes' ?
`${rsvp.guest_count} (${rsvp.guest_names.replace(/\s+/g, ', ')})` :
'No'
}
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(() => {
let items: string[] = [];
try {
if (typeof rsvp.items_bringing === 'string') {
try {
const parsed = JSON.parse(rsvp.items_bringing);
items = Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('Error parsing items_bringing JSON in table:', e);
}
} else if (Array.isArray(rsvp.items_bringing)) {
items = rsvp.items_bringing;
}
} catch (e) {
console.error('Error processing items in table:', e);
}
return items.length > 0 ? items.map((item: string, index: number) => (
<Chip
key={`${item}-${index}`}
label={item}
color="success"
size="small"
variant={claimedItems.includes(item) ? "filled" : "outlined"}
/>
)) : (
<Typography variant="body2" color="text.secondary">
No items
</Typography>
);
})()}
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Container>
);
};
export default EventView;