From 757209310ff8bf93879cbd4eef9fe332d563fbbd Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Tue, 25 Nov 2025 08:07:51 -0500 Subject: [PATCH] Updated description box and fixed title renaming --- backend/src/index.ts | 24 ++- frontend/package.json | 4 +- frontend/src/components/EventAdmin.tsx | 81 +++++---- frontend/src/components/EventDetails.tsx | 13 +- frontend/src/components/EventForm.tsx | 36 ++-- frontend/src/components/RichTextEditor.tsx | 195 +++++++++++++++++++++ 6 files changed, 290 insertions(+), 63 deletions(-) create mode 100644 frontend/src/components/RichTextEditor.tsx diff --git a/backend/src/index.ts b/backend/src/index.ts index f028931..2fcce25 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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) { diff --git a/frontend/package.json b/frontend/package.json index 490e23e..8f06cd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,13 +16,15 @@ "@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": "^2.0.0" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", "@types/axios": "^0.14.0", + "@types/react-quill": "^1.3.10", "typescript": "^4.9.5", "react-scripts": "5.0.1" }, diff --git a/frontend/src/components/EventAdmin.tsx b/frontend/src/components/EventAdmin.tsx index 18dc030..1dca0ee 100644 --- a/frontend/src/components/EventAdmin.tsx +++ b/frontend/src/components/EventAdmin.tsx @@ -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'); @@ -1233,13 +1248,12 @@ const EventAdmin: React.FC = () => { onChange={(e) => setUpdateForm(prev => ({ ...prev, title: e.target.value }))} fullWidth /> - 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" /> { /> {updateForm.event_conclusion_email_enabled && ( - 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 }} - /> + + 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." + /> + )} diff --git a/frontend/src/components/EventDetails.tsx b/frontend/src/components/EventDetails.tsx index 55fd47a..0244562 100644 --- a/frontend/src/components/EventDetails.tsx +++ b/frontend/src/components/EventDetails.tsx @@ -107,9 +107,16 @@ const EventDetails: React.FC = () => { {event.title} - - {event.description} - + Date: {new Date(event.date).toLocaleDateString()} diff --git a/frontend/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx index 006480e..9e63b27 100644 --- a/frontend/src/components/EventForm.tsx +++ b/frontend/src/components/EventForm.tsx @@ -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 /> - setFormData((prev) => ({ ...prev, description: value }))} + label="Description" + placeholder="Enter event description with formatting..." + theme="dark" /> { /> {formData.event_conclusion_email_enabled && ( - + + 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." + /> + )} diff --git a/frontend/src/components/RichTextEditor.tsx b/frontend/src/components/RichTextEditor.tsx new file mode 100644 index 0000000..b67adc2 --- /dev/null +++ b/frontend/src/components/RichTextEditor.tsx @@ -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 = ({ + 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 ( + + {label && ( + + {label} + + )} + + + + {helperText && ( + + {helperText} + + )} + + ); +}; + +export default RichTextEditor;