Compare commits

...

36 Commits

Author SHA1 Message Date
Ryderjj89
6e682188bb More display fixes 2025-11-25 08:44:33 -05:00
Ryderjj89
021edda0dc Fixed description display again 2025-11-25 08:35:48 -05:00
Ryderjj89
453d4eb5c6 Fixed description display 2025-11-25 08:28:35 -05:00
Ryderjj89
0f631c8755 Newer react quill 2025-11-25 08:12:32 -05:00
Ryderjj89
e6002d6865 Merge branch 'main' of https://git.rydertech.us/ryder/rsvp-manager 2025-11-25 08:08:20 -05:00
Ryderjj89
757209310f Updated description box and fixed title renaming 2025-11-25 08:07:51 -05:00
049c203f25 Update image url 2025-11-09 18:47:01 -05:00
Ryderjj89
0991e17670 updated git url in readme 2025-11-09 18:10:32 -05:00
Ryderjj89
f241b4a2e7 Updated image url 2025-11-09 17:47:08 -05:00
Ryderjj89
53340fc210 selected items fix 2025-10-24 18:41:48 -04:00
Ryderjj89
bd8f0fa2cd added selected items to rsvp form 2025-10-24 18:27:23 -04:00
Ryderjj89
ecd53fbadb feat: improve guest selection in rsvp form 2025-06-24 17:17:39 -04:00
Joshua Ryder
630cf7be41 Merge pull request #21 from Ryderjj89/dev
Fixing back to events button on thank you page
2025-06-04 19:15:11 -04:00
Ryderjj89
9760eccf4c Fixing back to events button on thank you page 2025-06-04 19:14:02 -04:00
Ryderjj89
54d0e8aab4 Fixing buttons on the Thank you page 2025-06-04 19:07:52 -04:00
Joshua Ryder
5d67d42205 Merge pull request #20 from Ryderjj89/dev
Fixing buttons on the Thank you page
2025-06-04 19:07:34 -04:00
Ryderjj89
becc530c18 Spacing issue with guest names 2025-06-04 18:53:27 -04:00
Joshua Ryder
2587a5663b Merge pull request #19 from Ryderjj89/dev
Spacing issue with guest names
2025-06-04 18:53:10 -04:00
Ryderjj89
3af12b431b Rearranging event details & more width for view/manage pages 2025-06-04 18:45:50 -04:00
Joshua Ryder
f366a9e4f3 Merge pull request #18 from Ryderjj89/dev
Rearranging event details & more width for view/manage pages
2025-06-04 18:45:28 -04:00
Joshua Ryder
044b7f2cde Merge pull request #17 from Ryderjj89/dev
Button spacing and formatting for rsvp form
2025-06-04 18:35:40 -04:00
Joshua Ryder
88c06e06f6 Merge pull request #16 from Ryderjj89/dev
Button spacing and formatting for rsvp form
2025-06-04 18:29:45 -04:00
Joshua Ryder
e0a3f3f889 Merge pull request #15 from Ryderjj89/dev
Back to Events button and RSVP form details
2025-06-04 18:19:25 -04:00
Joshua Ryder
aa69252657 Merge pull request #14 from Ryderjj89/dev
Changing card actions to grid
2025-06-04 18:06:55 -04:00
Joshua Ryder
01939f5881 Merge pull request #13 from Ryderjj89/dev
Flex spacing changes on the event cards for smaller screens
2025-06-04 18:02:07 -04:00
Joshua Ryder
2b6f4cfd2f Merge pull request #12 from Ryderjj89/dev
Padding and spacing fixes
2025-06-04 17:54:30 -04:00
Joshua Ryder
16db01292c Merge pull request #11 from Ryderjj89/dev
Updated docker compose for new volume mounting for the database
2025-06-04 17:41:50 -04:00
Joshua Ryder
52cacc8646 Merge pull request #10 from Ryderjj89/dev
Rearranged how the sqlite database is stored for better volume mounting
2025-06-04 17:39:04 -04:00
Joshua Ryder
4546185a89 Merge pull request #9 from Ryderjj89/dev
Fixing snackbar issue
2025-06-04 17:26:19 -04:00
Joshua Ryder
4722aeeb22 Merge pull request #8 from Ryderjj89/dev
Fixing snackbar issue
2025-06-04 17:21:20 -04:00
Joshua Ryder
4e66ce876d Merge pull request #7 from Ryderjj89/dev
Updating frontend to have a copy link to RSVP and improvements to the…
2025-06-04 17:16:47 -04:00
Joshua Ryder
482718050d Merge pull request #6 from Ryderjj89/dev
Event conclusion message, calendar invites, & more attendee management
2025-06-03 17:22:01 -04:00
Joshua Ryder
b5ecb32893 Merge pull request #5 from Ryderjj89/dev
Update README.md
2025-05-25 19:51:58 -04:00
Joshua Ryder
4d4920a751 Update README.md
More info about email notifications
2025-05-25 19:49:03 -04:00
Joshua Ryder
3521643196 Update README.md 2025-05-24 17:32:04 -04:00
Joshua Ryder
64db6c4c08 Merge pull request #4 from Ryderjj89/dev
Update README.md
2025-05-16 21:59:22 -04:00
11 changed files with 523 additions and 210 deletions

View File

@@ -61,15 +61,6 @@ This project was created completely by the [Cursor AI Code Editor](https://www.c
### Installation
#### Branch Selection
There are 2 branches, latest & dev.
| Branch | Description |
| ------------- | ------------- |
| Latest | The most recent stable build. Use this if you don't like to get changes early. |
| Dev | Use this if you want to be on the cutting edge. This can be unstable or even broken. |
#### Environment Variables
These variables below are all for the email notifications. If you want to be able to send email notifications correctly, each of these needs to be provided and filled out.
@@ -113,7 +104,7 @@ docker run -d --name rsvp-manager \
-e FRONTEND_BASE_URL=https://rsvp.example.com \
-e TZ=<CHANGE THIS!>
--restart unless-stopped \
ryderjj89/rsvp-manager:<CHANGE THIS TAG!>
git.rydertech.us/ryder/rsvp-manager:latest
```
2. Access the application at `http://localhost:3000`

View File

@@ -835,6 +835,19 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
return res.status(404).json({ error: 'Event not found' });
}
// Generate new slug if title changed
let newSlug = slug;
if (title && title !== eventRows[0].title) {
newSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
// Check if new slug already exists (and it's not the current event)
const existingEvent = await db.get('SELECT id FROM events WHERE slug = ? AND id != ?', [newSlug, eventRows[0].id]);
if (existingEvent) {
// Append a timestamp to make it unique
newSlug = `${newSlug}-${Date.now()}`;
}
}
// Ensure needed_items is properly formatted
let parsedNeededItems: string[] = [];
try {
@@ -886,9 +899,9 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
wallpaperPath = req.file.filename;
}
// Update the event
// Update the event (including the slug if it changed)
await db.run(
'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ?, max_guests_per_rsvp = ?, email_notifications_enabled = ?, email_recipients = ?, event_conclusion_email_enabled = ?, event_conclusion_message = ? WHERE slug = ?',
'UPDATE events SET title = ?, description = ?, date = ?, location = ?, needed_items = ?, rsvp_cutoff_date = ?, wallpaper = ?, max_guests_per_rsvp = ?, email_notifications_enabled = ?, email_recipients = ?, event_conclusion_email_enabled = ?, event_conclusion_message = ?, slug = ? WHERE slug = ?',
[
title ?? eventRows[0].title,
description === undefined ? eventRows[0].description : description,
@@ -902,12 +915,13 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
emailRecipients,
eventConclusionEmailEnabled ? 1 : 0, // Update new field
eventConclusionMessage, // Update new field
slug
newSlug, // Update slug if it changed
slug // WHERE clause uses the old slug
]
);
// Get the updated event
const updatedEvent = await db.get('SELECT * FROM events WHERE slug = ?', [slug]);
// Get the updated event using the new slug
const updatedEvent = await db.get('SELECT * FROM events WHERE slug = ?', [newSlug]);
// Add the full path to the wallpaper and parse JSON/boolean fields
if (updatedEvent.wallpaper) {

View File

@@ -1,7 +1,7 @@
services:
app:
container_name: rsvp_manager
image: ryderjj89/rsvp-manager:latest
image: git.rydertech.us/rydertech/rsvp-manager:latest
ports:
- "3000:3000"
volumes:

View File

@@ -16,7 +16,8 @@
"@mui/material": "^5.14.2",
"@mui/icons-material": "^5.14.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0"
"@emotion/styled": "^11.11.0",
"react-quill-new": "^3.3.3"
},
"devDependencies": {
"@types/react": "^18.2.15",

View File

@@ -39,6 +39,7 @@ import WallpaperIcon from '@mui/icons-material/Wallpaper';
import EmailIcon from '@mui/icons-material/Email';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import axios from 'axios';
import RichTextEditor from './RichTextEditor';
interface RSVP {
id: number;
@@ -621,11 +622,11 @@ const EventAdmin: React.FC = () => {
try {
// Create FormData and append all fields
const formData = new FormData();
formData.append('title', updateForm.title); // Submit the updated title
formData.append('description', updateForm.description);
formData.append('location', updateForm.location);
formData.append('date', updateForm.date);
formData.append('rsvp_cutoff_date', updateForm.rsvp_cutoff_date);
formData.append('title', event.title); // Keep existing title
formData.append('needed_items', JSON.stringify(event.needed_items)); // Keep existing needed items
formData.append('email_notifications_enabled', updateForm.email_notifications_enabled.toString());
formData.append('email_recipients', updateForm.email_recipients);
@@ -644,19 +645,33 @@ const EventAdmin: React.FC = () => {
'Content-Type': 'multipart/form-data',
},
});
setEvent(prev => prev ? {
...prev,
description: updateForm.description,
location: updateForm.location,
date: updateForm.date,
rsvp_cutoff_date: updateForm.rsvp_cutoff_date,
wallpaper: response.data.wallpaper || prev.wallpaper,
email_notifications_enabled: updateForm.email_notifications_enabled,
email_recipients: updateForm.email_recipients
} : null);
setUpdateInfoDialogOpen(false);
// Check if slug changed (title was updated)
const newSlug = response.data.slug;
if (newSlug && newSlug !== slug) {
// Slug changed - redirect to new URL
setUpdateInfoDialogOpen(false);
navigate(`/admin/events/${newSlug}`, { replace: true });
// Refresh the page to load the new event data
window.location.href = `/admin/events/${newSlug}`;
} else {
// Slug didn't change - just update local state
setEvent(prev => prev ? {
...prev,
title: updateForm.title,
description: updateForm.description,
location: updateForm.location,
date: updateForm.date,
rsvp_cutoff_date: updateForm.rsvp_cutoff_date,
wallpaper: response.data.wallpaper || prev.wallpaper,
email_notifications_enabled: updateForm.email_notifications_enabled,
email_recipients: updateForm.email_recipients,
event_conclusion_email_enabled: updateForm.event_conclusion_email_enabled,
event_conclusion_message: updateForm.event_conclusion_message
} : null);
setUpdateInfoDialogOpen(false);
}
} catch (error) {
console.error('Error updating event:', error);
setError('Failed to update event information');
@@ -729,7 +744,7 @@ const EventAdmin: React.FC = () => {
if (loading) {
return (
<Container maxWidth="lg">
<Container maxWidth="xl">
<Typography>Loading...</Typography>
</Container>
);
@@ -737,7 +752,7 @@ const EventAdmin: React.FC = () => {
if (error || !event) {
return (
<Container maxWidth="lg">
<Container maxWidth="xl">
<Typography color="error">{error || 'Event not found'}</Typography>
</Container>
);
@@ -763,7 +778,7 @@ const EventAdmin: React.FC = () => {
}}
>
<Box sx={{ py: 4 }}>
<Container maxWidth="lg">
<Container maxWidth="xl">
<Paper elevation={3} sx={{ p: { xs: 2, sm: 4 }, mt: 4 }}>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" component="h2" color="primary" gutterBottom>
@@ -824,18 +839,31 @@ const EventAdmin: React.FC = () => {
<Box sx={{ mb: 4 }}>
<Typography variant="subtitle1" gutterBottom>
<strong>Info:</strong> {event.description || 'None'}
<strong>Info:</strong>
</Typography>
<Box
sx={{
mb: 2,
pl: 2,
'& p': { margin: 0, lineHeight: 1.5 },
'& br': { display: 'block', content: '""', marginTop: '0.5em' },
'& ul, & ol': { marginLeft: 2, marginTop: '0.5em', marginBottom: '0.5em', paddingLeft: '1.5em' },
'& li': { marginBottom: 0, lineHeight: 1.5 },
'& h1, & h2, & h3': { marginTop: '0.75em', marginBottom: '0.5em' },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description || 'None' }}
/>
<Typography variant="subtitle1" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>Location:</strong> {event.location}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography>
{event.rsvp_cutoff_date && (
<Typography variant="subtitle1" gutterBottom>
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
)}
</Box>
@@ -945,9 +973,9 @@ const EventAdmin: React.FC = () => {
<TableCell>
{rsvp.bringing_guests === 'yes' ?
`${rsvp.guest_count || 0} (${Array.isArray(rsvp.guest_names) ?
rsvp.guest_names.join(', ') :
rsvp.guest_names.map(name => name.trim()).join(', ') :
typeof rsvp.guest_names === 'string' ?
rsvp.guest_names.replace(/\s+/g, ', ') :
rsvp.guest_names.replace(/\s+/g, ', ').trim() :
'No names provided'})` :
'No'
}
@@ -1233,13 +1261,12 @@ const EventAdmin: React.FC = () => {
onChange={(e) => setUpdateForm(prev => ({ ...prev, title: e.target.value }))}
fullWidth
/>
<TextField
label="Description"
<RichTextEditor
value={updateForm.description}
onChange={(e) => setUpdateForm(prev => ({ ...prev, description: e.target.value }))}
fullWidth
multiline
rows={3}
onChange={(value) => setUpdateForm(prev => ({ ...prev, description: value }))}
label="Description"
placeholder="Enter event description with formatting..."
theme="light"
/>
<TextField
label="Location"
@@ -1320,20 +1347,19 @@ const EventAdmin: React.FC = () => {
/>
{updateForm.event_conclusion_email_enabled && (
<TextField
fullWidth
label="Event conclusion message"
value={updateForm.event_conclusion_message}
onChange={(e) => setUpdateForm(prev => ({
...prev,
event_conclusion_message: e.target.value
}))}
variant="outlined"
multiline
rows={4}
helperText="This message will be sent to attendees who opted for email notifications the day after the event."
sx={{ mt: 2 }}
/>
<Box sx={{ mt: 2 }}>
<RichTextEditor
value={updateForm.event_conclusion_message}
onChange={(value) => setUpdateForm(prev => ({
...prev,
event_conclusion_message: value
}))}
label="Event conclusion message"
placeholder="Enter the message to send after the event..."
theme="light"
helperText="This message will be sent to attendees who opted for email notifications the day after the event."
/>
</Box>
)}
</Box>

View File

@@ -107,9 +107,17 @@ const EventDetails: React.FC = () => {
<Typography variant="h4" component="h1" gutterBottom>
{event.title}
</Typography>
<Typography variant="body1" paragraph>
{event.description}
</Typography>
<Box
sx={{
'& p': { margin: 0, lineHeight: 1.5 },
'& br': { display: 'block', content: '""', marginTop: '0.5em' },
'& ul, & ol': { marginLeft: 2, marginTop: '0.5em', marginBottom: '0.5em', paddingLeft: '1.5em' },
'& li': { marginBottom: 0, lineHeight: 1.5 },
'& h1, & h2, & h3': { marginTop: '0.75em', marginBottom: '0.5em' },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description }}
/>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" component="div" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleDateString()}

View File

@@ -18,6 +18,7 @@ import WallpaperIcon from '@mui/icons-material/Wallpaper';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import axios from 'axios';
import RichTextEditor from './RichTextEditor';
const DarkTextField = styled(TextField)({
'& .MuiOutlinedInput-root': {
@@ -185,15 +186,12 @@ const EventForm: React.FC = () => {
variant="outlined"
required
/>
<DarkTextField
fullWidth
label="Description"
name="description"
<RichTextEditor
value={formData.description}
onChange={handleChange}
variant="outlined"
multiline
rows={4}
onChange={(value) => setFormData((prev) => ({ ...prev, description: value }))}
label="Description"
placeholder="Enter event description with formatting..."
theme="dark"
/>
<DarkTextField
fullWidth
@@ -435,18 +433,16 @@ const EventForm: React.FC = () => {
/>
{formData.event_conclusion_email_enabled && (
<DarkTextField
fullWidth
label="Event conclusion message"
name="event_conclusion_message" // Corrected name prop
value={formData.event_conclusion_message} // Corrected value prop
onChange={handleChange}
variant="outlined"
multiline
rows={4}
helperText="This message will be sent to attendees who opted for email notifications the day after the event."
sx={{ mt: 2 }} // Added margin top for spacing
/>
<Box sx={{ mt: 2 }}>
<RichTextEditor
value={formData.event_conclusion_message}
onChange={(value) => setFormData((prev) => ({ ...prev, event_conclusion_message: value }))}
label="Event conclusion message"
placeholder="Enter the message to send after the event..."
theme="dark"
helperText="This message will be sent to attendees who opted for email notifications the day after the event."
/>
</Box>
)}
</Box>
</Box>

View File

@@ -153,9 +153,26 @@ const EventList: React.FC = () => {
/>
</Box>
{event.description && (
<Typography variant="body2" color="text.secondary">
<strong>Info:</strong> {event.description}
</Typography>
<>
<Typography variant="body2" color="text.secondary" gutterBottom>
<strong>Info:</strong>
</Typography>
<Box
sx={{
pl: 2,
mb: 1,
color: 'text.secondary',
fontSize: '0.875rem',
'& p': { margin: 0, lineHeight: 1.5 },
'& br': { display: 'block', content: '""', marginTop: '0.5em' },
'& ul, & ol': { marginLeft: 2, marginTop: '0.5em', marginBottom: '0.5em', paddingLeft: '1.5em' },
'& li': { marginBottom: 0, lineHeight: 1.5 },
'& h1, & h2, & h3': { marginTop: '0.75em', marginBottom: '0.25em', fontSize: '1rem' },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description }}
/>
</>
)}
<Typography variant="body2" color="text.secondary">
<strong>Date:</strong> {new Date(event.date).toLocaleString()}

View File

@@ -126,7 +126,7 @@ const EventView: React.FC = () => {
if (loading) {
return (
<Container maxWidth="lg">
<Container maxWidth="xl">
<Typography>Loading...</Typography>
</Container>
);
@@ -134,7 +134,7 @@ const EventView: React.FC = () => {
if (error || !event) {
return (
<Container maxWidth="lg">
<Container maxWidth="xl">
<Typography color="error">{error || 'Event not found'}</Typography>
</Container>
);
@@ -160,7 +160,7 @@ const EventView: React.FC = () => {
}}
>
<Box sx={{ py: 4 }}>
<Container maxWidth="lg">
<Container maxWidth="xl">
<Paper elevation={3} sx={{ p: { xs: 2, sm: 4 }, mt: 4 }}>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" component="h2" color="primary" gutterBottom>
@@ -187,18 +187,31 @@ const EventView: React.FC = () => {
<Box sx={{ mb: 4 }}>
<Typography variant="subtitle1" gutterBottom>
<strong>Info:</strong> {event.description || 'None'}
<strong>Info:</strong>
</Typography>
<Box
sx={{
mb: 2,
pl: 2,
'& p': { margin: 0, lineHeight: 1.5 },
'& br': { display: 'block', content: '""', marginTop: '0.5em' },
'& ul, & ol': { marginLeft: 2, marginTop: '0.5em', marginBottom: '0.5em', paddingLeft: '1.5em' },
'& li': { marginBottom: 0, lineHeight: 1.5 },
'& h1, & h2, & h3': { marginTop: '0.75em', marginBottom: '0.5em' },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description || 'None' }}
/>
<Typography variant="subtitle1" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>Location:</strong> {event.location}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleString()}
</Typography>
{event.rsvp_cutoff_date && (
<Typography variant="subtitle1" gutterBottom>
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<strong>RSVP cut-off date:</strong> {new Date(event.rsvp_cutoff_date).toLocaleString()}
</Typography>
)}
</Box>
@@ -293,9 +306,9 @@ const EventView: React.FC = () => {
<TableCell>
{rsvp.bringing_guests === 'yes' ?
`${rsvp.guest_count} (${Array.isArray(rsvp.guest_names) ?
rsvp.guest_names.join(', ') :
rsvp.guest_names.map(name => name.trim()).join(', ') :
typeof rsvp.guest_names === 'string' ?
rsvp.guest_names.replace(/\s+/g, ', ') :
rsvp.guest_names.replace(/\s+/g, ', ').trim() :
'No names provided'})` :
'No'
}
@@ -350,4 +363,4 @@ const EventView: React.FC = () => {
);
};
export default EventView;
export default EventView;

View File

@@ -399,19 +399,19 @@ const RSVPForm: React.FC = () => {
</Typography>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
<Button
variant="contained"
variant="outlined"
color="primary"
onClick={() => navigate('/')}
>
Back to Events
</Button>
<Button
variant="outlined"
variant="contained"
color="primary"
onClick={() => navigate('/')}
onClick={() => navigate(`/view/events/${slug}`)}
sx={{ flexGrow: 1 }}
>
Back to Events
View RSVPs
</Button>
</Box>
</Paper>
@@ -453,9 +453,25 @@ const RSVPForm: React.FC = () => {
{event.title}
</Typography>
{event.description && (
<Typography variant="body2" color="text.secondary">
<strong>Info:</strong> {event.description}
</Typography>
<>
<Typography variant="body2" color="text.secondary" gutterBottom>
<strong>Info:</strong>
</Typography>
<Box
sx={{
pl: 2,
color: 'text.secondary',
fontSize: '0.875rem',
'& p': { margin: 0, lineHeight: 1.5 },
'& br': { display: 'block', content: '""', marginTop: '0.5em' },
'& ul, & ol': { marginLeft: 2, marginTop: '0.5em', marginBottom: '0.5em', paddingLeft: '1.5em' },
'& li': { marginBottom: 0, lineHeight: 1.5 },
'& h1, & h2, & h3': { marginTop: '0.75em', marginBottom: '0.5em' },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description }}
/>
</>
)}
{event.description && <Divider sx={{ my: 2 }} />} {/* Separator after Info */}
<Box> {/* Grouping Date, Location, and Note */}
@@ -538,51 +554,69 @@ const RSVPForm: React.FC = () => {
{formData.bringing_guests === 'yes' && (
<>
<TextField
label="Number of Guests"
name="guest_count"
type="number"
value={formData.guest_count}
onChange={(e) => {
const value = parseInt(e.target.value);
if (isNaN(value)) return;
// Check if there's a maximum guest limit
const maxGuests = event?.max_guests_per_rsvp;
let newCount = value;
// If max_guests_per_rsvp is set and not -1 (unlimited), enforce the limit
if (maxGuests !== undefined && maxGuests !== -1 && value > maxGuests) {
newCount = maxGuests;
}
// Ensure count is at least 1
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`
}
/>
{/* Render dropdown if there's a max limit, otherwise render number input */}
{event?.max_guests_per_rsvp !== undefined && event?.max_guests_per_rsvp !== -1 ? (
<FormControl fullWidth required>
<InputLabel>Number of Guests</InputLabel>
<Select
name="guest_count"
value={formData.guest_count.toString()}
onChange={(e) => {
const value = parseInt(e.target.value);
setFormData(prev => ({
...prev,
guest_count: value,
guest_names: Array(value).fill('').map((_, i) => prev.guest_names[i] || '')
}));
}}
label="Number of Guests"
required
>
{Array.from({ length: event.max_guests_per_rsvp }, (_, i) => i + 1).map((num) => (
<MenuItem key={num} value={num.toString()}>
{num}
</MenuItem>
))}
</Select>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, ml: 1.75 }}>
Maximum {event.max_guests_per_rsvp} additional guests allowed
</Typography>
</FormControl>
) : (
<TextField
label="Number of Guests"
name="guest_count"
value={formData.guest_count}
onChange={(e) => {
const value = e.target.value;
// Only allow numbers
if (!/^\d*$/.test(value)) return;
const numValue = parseInt(value) || 0;
// Ensure count is at least 1 when not empty
const newCount = value === '' ? 0 : Math.max(1, numValue);
setFormData(prev => ({
...prev,
guest_count: newCount,
guest_names: Array(newCount).fill('').map((_, i) => prev.guest_names[i] || '')
}));
}}
fullWidth
variant="outlined"
required
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*'
}}
error={formData.guest_count < 1}
helperText={
formData.guest_count < 1
? "Number of guests must be at least 1"
: "No limit on number of guests"
}
/>
)}
{Array.from({ length: formData.guest_count }).map((_, index) => (
<TextField
@@ -599,65 +633,83 @@ const RSVPForm: React.FC = () => {
)}
{neededItems.length > 0 && (
<FormControl fullWidth>
<InputLabel>What items are you bringing?</InputLabel>
<Select
multiple
name="items_bringing"
value={formData.items_bringing}
onChange={handleItemsChange}
input={<OutlinedInput label="What items are you bringing?" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
<>
{claimedItems.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 500 }}>
Items already being brought:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{claimedItems.map((item) => (
<Chip
key={item}
label={item}
color="success"
/>
))}
</Box>
)}
open={isItemsSelectOpen} // Control open state
onOpen={() => setIsItemsSelectOpen(true)} // Set open when opened
onClose={() => setIsItemsSelectOpen(false)} // Set closed when closed
MenuProps={{
PaperProps: {
sx: {
maxHeight: 300, // Limit height of the dropdown
overflowY: 'auto',
},
},
MenuListProps: {
sx: {
},
},
}}
>
{neededItems.map((item) => (
<MenuItem key={item} value={item}>
<Checkbox checked={formData.items_bringing.includes(item)} />
<ListItemText primary={item} />
</MenuItem>
))}
<Box sx={{
position: 'sticky',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'background.paper',
padding: 1,
zIndex: 1,
borderTop: '1px solid',
borderColor: 'divider',
textAlign: 'center',
}}>
<Button
variant="contained"
onClick={() => setIsItemsSelectOpen(false)}
fullWidth
>
Done
</Button>
</Box>
</Select>
</FormControl>
)}
<FormControl fullWidth>
<InputLabel>What items are you bringing?</InputLabel>
<Select
multiple
name="items_bringing"
value={formData.items_bringing}
onChange={handleItemsChange}
input={<OutlinedInput label="What items are you bringing?" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
open={isItemsSelectOpen} // Control open state
onOpen={() => setIsItemsSelectOpen(true)} // Set open when opened
onClose={() => setIsItemsSelectOpen(false)} // Set closed when closed
MenuProps={{
PaperProps: {
sx: {
maxHeight: 300, // Limit height of the dropdown
overflowY: 'auto',
},
},
MenuListProps: {
sx: {
},
},
}}
>
{neededItems.map((item) => (
<MenuItem key={item} value={item}>
<Checkbox checked={formData.items_bringing.includes(item)} />
<ListItemText primary={item} />
</MenuItem>
))}
<Box sx={{
position: 'sticky',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'background.paper',
padding: 1,
zIndex: 1,
borderTop: '1px solid',
borderColor: 'divider',
textAlign: 'center',
}}>
<Button
variant="contained"
onClick={() => setIsItemsSelectOpen(false)}
fullWidth
>
Done
</Button>
</Box>
</Select>
</FormControl>
</>
)}
<TextField

View File

@@ -0,0 +1,195 @@
import React from 'react';
import ReactQuill from 'react-quill-new';
import 'react-quill-new/dist/quill.snow.css';
import { Box, Typography, styled } from '@mui/material';
const StyledEditorContainer = styled(Box)(({ theme }) => ({
'& .quill': {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: theme.shape.borderRadius,
border: '1px solid rgba(255, 255, 255, 0.23)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.4)',
},
'&:focus-within': {
borderColor: theme.palette.primary.main,
borderWidth: '2px',
},
},
'& .ql-toolbar': {
backgroundColor: 'rgba(255, 255, 255, 0.08)',
borderTopLeftRadius: theme.shape.borderRadius,
borderTopRightRadius: theme.shape.borderRadius,
border: 'none',
borderBottom: '1px solid rgba(255, 255, 255, 0.23)',
},
'& .ql-container': {
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
border: 'none',
fontFamily: theme.typography.fontFamily,
fontSize: '1rem',
minHeight: '150px',
},
'& .ql-editor': {
minHeight: '150px',
color: 'rgba(255, 255, 255, 0.9)',
'&.ql-blank::before': {
color: 'rgba(255, 255, 255, 0.5)',
fontStyle: 'normal',
},
},
'& .ql-stroke': {
stroke: 'rgba(255, 255, 255, 0.7) !important',
},
'& .ql-fill': {
fill: 'rgba(255, 255, 255, 0.7) !important',
},
'& .ql-picker-label': {
color: 'rgba(255, 255, 255, 0.7) !important',
},
'& .ql-picker-options': {
backgroundColor: 'rgb(30, 30, 30) !important',
border: '1px solid rgba(255, 255, 255, 0.23) !important',
},
'& .ql-picker-item': {
color: 'rgba(255, 255, 255, 0.9) !important',
'&:hover': {
color: '#90caf9 !important',
},
},
'& .ql-toolbar button:hover .ql-stroke': {
stroke: '#90caf9 !important',
},
'& .ql-toolbar button:hover .ql-fill': {
fill: '#90caf9 !important',
},
'& .ql-toolbar button.ql-active .ql-stroke': {
stroke: '#90caf9 !important',
},
'& .ql-toolbar button.ql-active .ql-fill': {
fill: '#90caf9 !important',
},
}));
const LightEditorContainer = styled(Box)(({ theme }) => ({
'& .quill': {
backgroundColor: '#fff',
borderRadius: theme.shape.borderRadius,
border: '1px solid rgba(0, 0, 0, 0.23)',
'&:hover': {
borderColor: 'rgba(0, 0, 0, 0.87)',
},
'&:focus-within': {
borderColor: theme.palette.primary.main,
borderWidth: '2px',
},
},
'& .ql-toolbar': {
backgroundColor: 'rgba(0, 0, 0, 0.02)',
borderTopLeftRadius: theme.shape.borderRadius,
borderTopRightRadius: theme.shape.borderRadius,
border: 'none',
borderBottom: '1px solid rgba(0, 0, 0, 0.12)',
},
'& .ql-container': {
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
border: 'none',
fontFamily: theme.typography.fontFamily,
fontSize: '1rem',
minHeight: '150px',
},
'& .ql-editor': {
minHeight: '150px',
color: 'rgba(0, 0, 0, 0.87)',
'&.ql-blank::before': {
color: 'rgba(0, 0, 0, 0.5)',
fontStyle: 'normal',
},
},
}));
interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
label?: string;
placeholder?: string;
theme?: 'dark' | 'light';
helperText?: string;
minHeight?: number;
}
const RichTextEditor: React.FC<RichTextEditorProps> = ({
value,
onChange,
label,
placeholder,
theme = 'dark',
helperText,
minHeight = 150,
}) => {
const modules = {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'indent': '-1' }, { 'indent': '+1' }],
['link'],
['clean'],
],
};
const formats = [
'header',
'bold', 'italic', 'underline', 'strike',
'list', 'bullet',
'indent',
'link',
];
const Container = theme === 'dark' ? StyledEditorContainer : LightEditorContainer;
return (
<Box sx={{ width: '100%' }}>
{label && (
<Typography
variant="subtitle2"
sx={{
mb: 1,
color: theme === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)',
fontSize: '0.875rem',
}}
>
{label}
</Typography>
)}
<Container>
<ReactQuill
value={value}
onChange={onChange}
modules={modules}
formats={formats}
placeholder={placeholder}
theme="snow"
style={{ minHeight: `${minHeight}px` }}
/>
</Container>
{helperText && (
<Typography
variant="caption"
sx={{
mt: 0.5,
ml: 1.5,
display: 'block',
color: theme === 'dark' ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)',
}}
>
{helperText}
</Typography>
)}
</Box>
);
};
export default RichTextEditor;