From 5b5649a17d1d7426fd3c828e7a69cfcd0278f4b7 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Wed, 7 May 2025 00:28:25 -0500 Subject: [PATCH] better blog editing adn email confirmations --- backend/src/routes/auth.js | 5 + backend/src/routes/stripePayment.js | 64 +++++ db/init/22-blog-json.sql | 1 + frontend/src/App.jsx | 2 + frontend/src/pages/Admin/BlogEditPage.jsx | 277 ++++++++++++++----- frontend/src/pages/Admin/BlogPreviewPage.jsx | 236 ++++++++++++++++ frontend/src/pages/BlogDetailPage.jsx | 56 +--- 7 files changed, 537 insertions(+), 104 deletions(-) create mode 100644 db/init/22-blog-json.sql create mode 100644 frontend/src/pages/Admin/BlogPreviewPage.jsx diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 840003b..a7cfc73 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -43,6 +43,11 @@ module.exports = (pool, query) => { [email] ); } + + await emailService.sendWelcomeEmail({ + to: email, + first_name: firstName + }); if(isSubscribed){ let subResult = await query( `INSERT INTO subscribers (id, email, first_name, last_name, status) diff --git a/backend/src/routes/stripePayment.js b/backend/src/routes/stripePayment.js index e352c5f..f39eb0b 100644 --- a/backend/src/routes/stripePayment.js +++ b/backend/src/routes/stripePayment.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const stripe = require('stripe'); const config = require('../config'); +const emailService = require('../services/emailService'); module.exports = (pool, query, authMiddleware) => { @@ -42,6 +43,69 @@ module.exports = (pool, query, authMiddleware) => { ); console.log(`Payment completed for order ${order_id}`); + + // Get order details for email confirmation + const orderResult = await query(` + SELECT o.*, + u.email, u.first_name, u.last_name, + o.shipping_address + FROM orders o + JOIN users u ON o.user_id = u.id + WHERE o.id = $1 + `, [order_id]); + + if (orderResult.rows.length > 0) { + const order = orderResult.rows[0]; + + // Get order items with product details + const itemsResult = await query(` + SELECT + oi.id, + oi.quantity, + oi.price_at_purchase, + p.name as product_name + FROM order_items oi + JOIN products p ON oi.product_id = p.id + WHERE oi.order_id = $1 + `, [order_id]); + + // Generate items HTML table for email + const itemsHtml = itemsResult.rows.map(item => ` + + ${item.product_name} + ${item.quantity} + $${parseFloat(item.price_at_purchase).toFixed(2)} + $${(parseFloat(item.price_at_purchase) * item.quantity).toFixed(2)} + + `).join(''); + + // Format shipping address for email + let shippingAddress = 'No shipping address provided'; + if (order.shipping_address) { + const address = typeof order.shipping_address === 'string' ? + JSON.parse(order.shipping_address) : order.shipping_address; + + shippingAddress = ` + ${address.name || ''}
+ ${address.street || ''}
+ ${address.city || ''}, ${address.state || ''} ${address.zip || ''}
+ ${address.country || ''} + `; + } + + // Send order confirmation email + await emailService.sendOrderConfirmation({ + to: order.email, + first_name: order.first_name || 'Customer', + order_id: order.id.substring(0, 8), // first 8 characters of UUID for cleaner display + order_date: new Date(order.created_at).toLocaleDateString(), + order_total: `$${parseFloat(order.total_amount).toFixed(2)}`, + shipping_address: shippingAddress, + items_html: itemsHtml + }); + + console.log(`Order confirmation email sent to ${order.email}`); + } } } break; diff --git a/db/init/22-blog-json.sql b/db/init/22-blog-json.sql new file mode 100644 index 0000000..04c7c03 --- /dev/null +++ b/db/init/22-blog-json.sql @@ -0,0 +1 @@ +ALTER TABLE blog_posts ADD COLUMN IF NOT EXISTS design JSONB; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f1fe796..b3b42c8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -42,6 +42,7 @@ const BlogPage = lazy(() => import('@pages/BlogPage')); const BlogDetailPage = lazy(() => import('@pages/BlogDetailPage')); const AdminBlogPage = lazy(() => import('@pages/Admin/BlogPage')); const BlogEditPage = lazy(() => import('@pages/Admin/BlogEditPage')); +const BlogPreviewPage = lazy(() => import('@pages/Admin/BlogPreviewPage')); const AdminBlogCommentsPage = lazy(() => import('@pages/Admin/BlogCommentsPage')); const AdminProductReviewsPage = lazy(() => import('@pages/Admin/ProductReviewsPage')); const EmailTemplatesPage = lazy(() => import('@pages/Admin/EmailTemplatesPage')); @@ -199,6 +200,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/Admin/BlogEditPage.jsx b/frontend/src/pages/Admin/BlogEditPage.jsx index 882098b..280caf3 100644 --- a/frontend/src/pages/Admin/BlogEditPage.jsx +++ b/frontend/src/pages/Admin/BlogEditPage.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Box, Typography, @@ -25,25 +25,32 @@ import { CardActions, Tooltip, Breadcrumbs, - Link + Link, + Accordion, + AccordionSummary, + AccordionDetails, } from '@mui/material'; import { ArrowBack as ArrowBackIcon, Save as SaveIcon, Preview as PreviewIcon, - Delete as DeleteIcon + Delete as DeleteIcon, + ExpandMore as ExpandMoreIcon, + Info as InfoIcon } from '@mui/icons-material'; import { useNavigate, useParams, Link as RouterLink } from 'react-router-dom'; import { useAdminBlogPost, useCreateBlogPost, useUpdateBlogPost, useBlogCategories } from '@hooks/blogHooks'; import { useAuth } from '@hooks/reduxHooks'; import ImageUploader from '@components/ImageUploader'; import imageUtils from '@utils/imageUtils'; +import EmailEditor from 'react-email-editor'; const BlogEditPage = () => { const { id } = useParams(); const navigate = useNavigate(); const isNewPost = !id; const { userData } = useAuth(); + const emailEditorRef = useRef(null); // Form state const [formData, setFormData] = useState({ @@ -54,13 +61,15 @@ const BlogEditPage = () => { tags: [], featuredImagePath: '', status: 'draft', - publishNow: false + publishNow: false, + design: null // New field to store email editor design JSON }); // Validation state const [errors, setErrors] = useState({}); const [notificationOpen, setNotificationOpen] = useState(false); const [notification, setNotification] = useState({ type: 'success', message: '' }); + const [isEditorReady, setIsEditorReady] = useState(false); // Fetch blog post if editing const { @@ -112,6 +121,120 @@ const BlogEditPage = () => { setFormData(prev => ({ ...prev, featuredImagePath: '' })); }; + // Email editor ready handler + const onEditorReady = () => { + setIsEditorReady(true); + console.log('Email editor is ready'); + + // If editing existing post with design data, load it + if (formData.design && emailEditorRef.current) { + emailEditorRef.current.editor.loadDesign(formData.design); + } + // If post has content but no design, create default design with the content + else if (formData.content && emailEditorRef.current) { + const defaultDesign = { + body: { + rows: [ + { + cells: [1], + columns: [ + { + contents: [ + { + type: "html", + values: { + html: formData.content, + containerPadding: "10px" + } + } + ] + } + ] + } + ] + } + }; + emailEditorRef.current.editor.loadDesign(defaultDesign); + } + // For new posts, load a simple default template + else if (emailEditorRef.current) { + const defaultTemplate = { + body: { + rows: [ + { + cells: [1], + columns: [ + { + contents: [ + { + type: "html", + values: { + containerPadding: "10px", + textAlign: "left", + html: `

Exploring the Wonders of Nature

+

A journey through fascinating landscapes and natural phenomena

+ +Beautiful mountain landscape + +

The Majesty of Mountains

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum. Cras porttitor metus justo, vitae facilisis massa tempus sit amet. Nullam sit amet ipsum at magna tempus dignissim. Donec et nisi sed dolor scelerisque bibendum ut sit amet dolor.

+

Proin gravida nibh vel velit auctor aliquet. Aenean sollicitudin, lorem quis bibendum auctor, nisi elit consequat ipsum, nec sagittis sem nibh id elit. Duis sed odio sit amet nibh vulputate cursus a sit amet mauris. Morbi accumsan ipsum velit. Nam nec tellus a odio tincidunt auctor a ornare odio.

+ +
+
+ Forest waterfall +
+
+

The Serenity of Waterfalls

+

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

+
+
+ +
+ +

Embracing Biodiversity

+

Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

+ +
+
+ Wildlife +

Wildlife in natural habitat

+
+
+ Insect +

Macro photography reveals tiny worlds

+
+
+ Flowers +

Vibrant flora adds color to our world

+
+
+ +
+

"In all things of nature there is something of the marvelous." — Aristotle

+
+ +

Conservation Efforts

+

Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?

+ + + +

Thank you for reading our blog post. Feel free to share it with others who might be interested in nature conservation.

` + } + } + ] + } + ] + } + ] + } + }; + emailEditorRef.current.editor.loadDesign(defaultTemplate); + } + }; + // Validate form const validateForm = () => { const newErrors = {}; @@ -120,10 +243,6 @@ const BlogEditPage = () => { newErrors.title = 'Title is required'; } - if (!formData.content.trim()) { - newErrors.content = 'Content is required'; - } - setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -140,39 +259,56 @@ const BlogEditPage = () => { setNotificationOpen(true); return; } - - try { - const formattedData = { - ...formData, - // Convert tag objects to string names if needed - tags: formData.tags.map(tag => typeof tag === 'string' ? tag : tag.name) - }; - - if (isNewPost) { - await createPost.mutateAsync(formattedData); - navigate('/admin/blog'); - } else { - await updatePost.mutateAsync({ id, postData: formattedData }); - setNotification({ - type: 'success', - message: 'Blog post updated successfully' - }); - setNotificationOpen(true); - } - } catch (error) { - setNotification({ - type: 'error', - message: error.message || 'An error occurred while saving the post' + + if (emailEditorRef.current) { + emailEditorRef.current.editor.exportHtml(async (data) => { + const { design, html } = data; + + try { + const formattedData = { + ...formData, + content: html, + design: design, + tags: formData.tags.map(tag => typeof tag === 'string' ? tag : tag.name) + }; + + if (isNewPost) { + await createPost.mutateAsync(formattedData); + navigate('/admin/blog'); + } else { + await updatePost.mutateAsync({ id, postData: formattedData }); + setNotification({ + type: 'success', + message: 'Blog post updated successfully' + }); + setNotificationOpen(true); + } + } catch (error) { + setNotification({ + type: 'error', + message: error.message || 'An error occurred while saving the post' + }); + setNotificationOpen(true); + } }); - setNotificationOpen(true); } }; // Preview post const handlePreview = () => { - // Store current form data in session storage - sessionStorage.setItem('blog-post-preview', JSON.stringify(formData)); - window.open('/admin/blog/preview', '_blank'); + if (emailEditorRef.current) { + emailEditorRef.current.editor.exportHtml((data) => { + const { html } = data; + + // Store current form data and HTML content in session storage + const previewData = { + ...formData, + content: html + }; + sessionStorage.setItem('blog-post-preview', JSON.stringify(previewData)); + window.open('/admin/blog/preview', '_blank'); + }); + } }; // Load post data when available @@ -186,7 +322,8 @@ const BlogEditPage = () => { tags: post.tags || [], featuredImagePath: post.featured_image_path || '', status: post.status || 'draft', - publishNow: false + publishNow: false, + design: post.design || null }); } }, [post, isNewPost]); @@ -366,30 +503,6 @@ const BlogEditPage = () => { )} - {/* Content */} - - - Post Content - - - - Pro tip: You can use HTML markup for formatting - - - {/* Excerpt */} { /> + {/* Email Editor Tips */} + + + }> + + + Editor Tips + + + + Tips for creating beautiful blog posts: +
    +
  • Use the visual editor to create rich, visually appealing content
  • +
  • You can drag and drop content blocks from the right panel
  • +
  • The editor supports images, buttons, columns, and more
  • +
  • For advanced users: You can switch to HTML mode to edit raw HTML
  • +
  • Add your own images by using the image upload button in the editor
  • +
  • Save often to avoid losing your work
  • +
+ + For best results, design your blog post in Figma first, then implement it here using the visual tools. + +
+
+
+ + {/* Content (Email Editor) */} + + + Post Content + + + + + + {/* Actions */} @@ -460,7 +613,7 @@ const BlogEditPage = () => { color="primary" startIcon={createPost.isLoading || updatePost.isLoading ? : } - disabled={createPost.isLoading || updatePost.isLoading} + disabled={createPost.isLoading || updatePost.isLoading || !isEditorReady} > {createPost.isLoading || updatePost.isLoading ? 'Saving...' : (isNewPost ? 'Create' : 'Update')} Post diff --git a/frontend/src/pages/Admin/BlogPreviewPage.jsx b/frontend/src/pages/Admin/BlogPreviewPage.jsx new file mode 100644 index 0000000..6b7dd99 --- /dev/null +++ b/frontend/src/pages/Admin/BlogPreviewPage.jsx @@ -0,0 +1,236 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + Paper, + Divider, + Chip, + Button, + CircularProgress, + Alert, + Container, + Breadcrumbs, + Link +} from '@mui/material'; +import { Link as RouterLink, useNavigate } from 'react-router-dom'; +import { format } from 'date-fns'; +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import EditIcon from '@mui/icons-material/Edit'; +import imageUtils from '@utils/imageUtils'; + +const BlogPreviewPage = () => { + const navigate = useNavigate(); + const [post, setPost] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Load the preview data from session storage + useEffect(() => { + try { + const previewData = sessionStorage.getItem('blog-post-preview'); + if (previewData) { + const parsedData = JSON.parse(previewData); + setPost(parsedData); + } else { + setError('No preview data found. Please go back and try again.'); + } + } catch (err) { + setError('Failed to load preview data: ' + err.message); + } finally { + setLoading(false); + } + }, []); + + // Format date + const formatDate = (dateString) => { + const date = dateString ? new Date(dateString) : new Date(); + return format(date, 'MMMM d, yyyy'); + }; + + // Handle edit button click + const handleEditClick = () => { + if (post?.id) { + navigate(`/admin/blog/edit/${post.id}`); + } else { + navigate('/admin/blog/create'); + } + }; + + // Loading state + if (loading) { + return ( + + + + ); + } + + // Error state + if (error) { + return ( + + {error} + + ); + } + + // If post isn't found + if (!post) { + return ( + + Preview data not found. Please go back and try again. + + ); + } + + return ( + + + {/* Preview header */} + + + Preview Mode + + + This is a preview of how your blog post will appear to readers. It is not published yet. + + + + + + + + {/* Breadcrumbs */} + } + aria-label="breadcrumb" + sx={{ mb: 3 }} + > + + Home + + + Blog + + + {post.title} + + + + {/* Post header */} + + {/* Category */} + {post.categoryId && ( + + )} + + {/* Title */} + + {post.title} + + + {/* Author and date */} + + Preview • {formatDate()} + + + {/* Tags */} + + {post.tags && post.tags.filter(Boolean).map((tag, index) => ( + + ))} + + + + {/* Featured image */} + {post.featuredImagePath && ( + + {post.title} + + )} + + {/* Post content */} + + + {/* Post content - rendered as HTML */} +
+ + + + {/* Comments section placeholder */} + + + Comments + + + + + + Comments will appear here when your post is published. + + + + + ); +}; + +export default BlogPreviewPage; \ No newline at end of file diff --git a/frontend/src/pages/BlogDetailPage.jsx b/frontend/src/pages/BlogDetailPage.jsx index 257aceb..7d8e6bc 100644 --- a/frontend/src/pages/BlogDetailPage.jsx +++ b/frontend/src/pages/BlogDetailPage.jsx @@ -232,54 +232,26 @@ const BlogDetailPage = () => { my: 2, fontStyle: 'italic', bgcolor: 'background.paper' + }, + '& table': { + width: '100%', + borderCollapse: 'collapse', + my: 2 + }, + '& th, & td': { + border: '1px solid', + borderColor: 'divider', + p: 1 + }, + '& th': { + bgcolor: 'background.paper', + fontWeight: 'bold' } }} > {/* Post content - rendered as HTML */}
- - {/* Post images */} - {post.images && post.images.length > 0 && ( - - - Gallery - - - {post.images.map((image) => ( - - - {image.caption - - {image.caption && ( - - {image.caption} - - )} - - ))} - - - )} {/* Comments section */}