Compare commits

...

2 Commits

Author SHA1 Message Date
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
6 changed files with 290 additions and 63 deletions

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' }); 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 // Ensure needed_items is properly formatted
let parsedNeededItems: string[] = []; let parsedNeededItems: string[] = [];
try { try {
@@ -886,9 +899,9 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
wallpaperPath = req.file.filename; wallpaperPath = req.file.filename;
} }
// Update the event // Update the event (including the slug if it changed)
await db.run( 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, title ?? eventRows[0].title,
description === undefined ? eventRows[0].description : description, description === undefined ? eventRows[0].description : description,
@@ -902,12 +915,13 @@ app.put('/api/events/:slug', upload.single('wallpaper'), async (req: MulterReque
emailRecipients, emailRecipients,
eventConclusionEmailEnabled ? 1 : 0, // Update new field eventConclusionEmailEnabled ? 1 : 0, // Update new field
eventConclusionMessage, // 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 // Get the updated event using the new slug
const updatedEvent = await db.get('SELECT * FROM events WHERE slug = ?', [slug]); const updatedEvent = await db.get('SELECT * FROM events WHERE slug = ?', [newSlug]);
// Add the full path to the wallpaper and parse JSON/boolean fields // Add the full path to the wallpaper and parse JSON/boolean fields
if (updatedEvent.wallpaper) { if (updatedEvent.wallpaper) {

View File

@@ -16,13 +16,15 @@
"@mui/material": "^5.14.2", "@mui/material": "^5.14.2",
"@mui/icons-material": "^5.14.2", "@mui/icons-material": "^5.14.2",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0" "@emotion/styled": "^11.11.0",
"react-quill": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/react-quill": "^1.3.10",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"react-scripts": "5.0.1" "react-scripts": "5.0.1"
}, },

View File

@@ -39,6 +39,7 @@ import WallpaperIcon from '@mui/icons-material/Wallpaper';
import EmailIcon from '@mui/icons-material/Email'; import EmailIcon from '@mui/icons-material/Email';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import axios from 'axios'; import axios from 'axios';
import RichTextEditor from './RichTextEditor';
interface RSVP { interface RSVP {
id: number; id: number;
@@ -621,11 +622,11 @@ const EventAdmin: React.FC = () => {
try { try {
// Create FormData and append all fields // Create FormData and append all fields
const formData = new FormData(); const formData = new FormData();
formData.append('title', updateForm.title); // Submit the updated title
formData.append('description', updateForm.description); formData.append('description', updateForm.description);
formData.append('location', updateForm.location); formData.append('location', updateForm.location);
formData.append('date', updateForm.date); formData.append('date', updateForm.date);
formData.append('rsvp_cutoff_date', updateForm.rsvp_cutoff_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('needed_items', JSON.stringify(event.needed_items)); // Keep existing needed items
formData.append('email_notifications_enabled', updateForm.email_notifications_enabled.toString()); formData.append('email_notifications_enabled', updateForm.email_notifications_enabled.toString());
formData.append('email_recipients', updateForm.email_recipients); formData.append('email_recipients', updateForm.email_recipients);
@@ -644,19 +645,33 @@ const EventAdmin: React.FC = () => {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}); });
setEvent(prev => prev ? { // Check if slug changed (title was updated)
...prev, const newSlug = response.data.slug;
description: updateForm.description, if (newSlug && newSlug !== slug) {
location: updateForm.location, // Slug changed - redirect to new URL
date: updateForm.date, setUpdateInfoDialogOpen(false);
rsvp_cutoff_date: updateForm.rsvp_cutoff_date, navigate(`/admin/events/${newSlug}`, { replace: true });
wallpaper: response.data.wallpaper || prev.wallpaper, // Refresh the page to load the new event data
email_notifications_enabled: updateForm.email_notifications_enabled, window.location.href = `/admin/events/${newSlug}`;
email_recipients: updateForm.email_recipients } else {
} : null); // Slug didn't change - just update local state
setEvent(prev => prev ? {
setUpdateInfoDialogOpen(false); ...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) { } catch (error) {
console.error('Error updating event:', error); console.error('Error updating event:', error);
setError('Failed to update event information'); setError('Failed to update event information');
@@ -1233,13 +1248,12 @@ const EventAdmin: React.FC = () => {
onChange={(e) => setUpdateForm(prev => ({ ...prev, title: e.target.value }))} onChange={(e) => setUpdateForm(prev => ({ ...prev, title: e.target.value }))}
fullWidth fullWidth
/> />
<TextField <RichTextEditor
label="Description"
value={updateForm.description} value={updateForm.description}
onChange={(e) => setUpdateForm(prev => ({ ...prev, description: e.target.value }))} onChange={(value) => setUpdateForm(prev => ({ ...prev, description: value }))}
fullWidth label="Description"
multiline placeholder="Enter event description with formatting..."
rows={3} theme="light"
/> />
<TextField <TextField
label="Location" label="Location"
@@ -1320,20 +1334,19 @@ const EventAdmin: React.FC = () => {
/> />
{updateForm.event_conclusion_email_enabled && ( {updateForm.event_conclusion_email_enabled && (
<TextField <Box sx={{ mt: 2 }}>
fullWidth <RichTextEditor
label="Event conclusion message" value={updateForm.event_conclusion_message}
value={updateForm.event_conclusion_message} onChange={(value) => setUpdateForm(prev => ({
onChange={(e) => setUpdateForm(prev => ({ ...prev,
...prev, event_conclusion_message: value
event_conclusion_message: e.target.value }))}
}))} label="Event conclusion message"
variant="outlined" placeholder="Enter the message to send after the event..."
multiline theme="light"
rows={4} helperText="This message will be sent to attendees who opted for email notifications the day after the event."
helperText="This message will be sent to attendees who opted for email notifications the day after the event." />
sx={{ mt: 2 }} </Box>
/>
)} )}
</Box> </Box>

View File

@@ -107,9 +107,16 @@ const EventDetails: React.FC = () => {
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
{event.title} {event.title}
</Typography> </Typography>
<Typography variant="body1" paragraph> <Box
{event.description} sx={{
</Typography> '& p': { marginBottom: 1 },
'& ul, & ol': { marginLeft: 2, marginBottom: 1 },
'& li': { marginBottom: 0.5 },
'& h1, & h2, & h3': { marginTop: 2, marginBottom: 1 },
'& a': { color: 'primary.main' },
}}
dangerouslySetInnerHTML={{ __html: event.description }}
/>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" component="div" gutterBottom> <Typography variant="subtitle1" component="div" gutterBottom>
<strong>Date:</strong> {new Date(event.date).toLocaleDateString()} <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 DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import axios from 'axios'; import axios from 'axios';
import RichTextEditor from './RichTextEditor';
const DarkTextField = styled(TextField)({ const DarkTextField = styled(TextField)({
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
@@ -185,15 +186,12 @@ const EventForm: React.FC = () => {
variant="outlined" variant="outlined"
required required
/> />
<DarkTextField <RichTextEditor
fullWidth
label="Description"
name="description"
value={formData.description} value={formData.description}
onChange={handleChange} onChange={(value) => setFormData((prev) => ({ ...prev, description: value }))}
variant="outlined" label="Description"
multiline placeholder="Enter event description with formatting..."
rows={4} theme="dark"
/> />
<DarkTextField <DarkTextField
fullWidth fullWidth
@@ -435,18 +433,16 @@ const EventForm: React.FC = () => {
/> />
{formData.event_conclusion_email_enabled && ( {formData.event_conclusion_email_enabled && (
<DarkTextField <Box sx={{ mt: 2 }}>
fullWidth <RichTextEditor
label="Event conclusion message" value={formData.event_conclusion_message}
name="event_conclusion_message" // Corrected name prop onChange={(value) => setFormData((prev) => ({ ...prev, event_conclusion_message: value }))}
value={formData.event_conclusion_message} // Corrected value prop label="Event conclusion message"
onChange={handleChange} placeholder="Enter the message to send after the event..."
variant="outlined" theme="dark"
multiline helperText="This message will be sent to attendees who opted for email notifications the day after the event."
rows={4} />
helperText="This message will be sent to attendees who opted for email notifications the day after the event." </Box>
sx={{ mt: 2 }} // Added margin top for spacing
/>
)} )}
</Box> </Box>
</Box> </Box>

View File

@@ -0,0 +1,195 @@
import React from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/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;