better blog editing adn email confirmations
This commit is contained in:
parent
2bdcbb17d9
commit
5b5649a17d
7 changed files with 537 additions and 104 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 => `
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${item.product_name}</td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">${item.quantity}</td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">$${parseFloat(item.price_at_purchase).toFixed(2)}</td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">$${(parseFloat(item.price_at_purchase) * item.quantity).toFixed(2)}</td>
|
||||
</tr>
|
||||
`).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 || ''}<br>
|
||||
${address.street || ''}<br>
|
||||
${address.city || ''}, ${address.state || ''} ${address.zip || ''}<br>
|
||||
${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;
|
||||
|
|
|
|||
1
db/init/22-blog-json.sql
Normal file
1
db/init/22-blog-json.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE blog_posts ADD COLUMN IF NOT EXISTS design JSONB;
|
||||
|
|
@ -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() {
|
|||
<Route path="coupons/:id" element={<CouponEditPage />} />
|
||||
<Route path="coupons/:id/redemptions" element={<CouponRedemptionsPage />} />
|
||||
<Route path="blog" element={<AdminBlogPage />} />
|
||||
<Route path="blog/preview" element={<BlogPreviewPage />} />
|
||||
<Route path="blog/new" element={<BlogEditPage />} />
|
||||
<Route path="blog/:id" element={<BlogEditPage />} />
|
||||
<Route path="blog-comments" element={<AdminBlogCommentsPage />} />
|
||||
|
|
|
|||
|
|
@ -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: `<h1 style="font-size: 30px; color: #3f51b5; margin-bottom: 10px;">Exploring the Wonders of Nature</h1>
|
||||
<p style="font-size: 18px; color: #757575; font-style: italic; margin-bottom: 30px;">A journey through fascinating landscapes and natural phenomena</p>
|
||||
|
||||
<img src="https://images.unsplash.com/photo-1506744038136-46273834b3fb" alt="Beautiful mountain landscape" width="100%" style="max-width: 100%;">
|
||||
|
||||
<h2 style="font-size: 28px; color: #4caf50; margin-top: 30px; margin-bottom: 15px;">The Majesty of Mountains</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 20px;">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.</p>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 20px;">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.</p>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div style="flex: 2; padding: 10px;">
|
||||
<img src="https://images.unsplash.com/photo-1565118531796-763e5082d113" alt="Forest waterfall" width="100%" style="max-width: 100%;">
|
||||
</div>
|
||||
<div style="flex: 1; padding: 20px;">
|
||||
<h3 style="font-size: 22px; color: #2196f3; margin-bottom: 15px;">The Serenity of Waterfalls</h3>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333333;">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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 20px 0;">
|
||||
|
||||
<h2 style="font-size: 28px; color: #ff9800; margin-top: 30px; margin-bottom: 15px;">Embracing Biodiversity</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 20px;">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.</p>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
|
||||
<div style="flex: 1; padding: 10px; max-width: 33%;">
|
||||
<img src="https://images.unsplash.com/photo-1535083783855-76ae62b2914e" alt="Wildlife" width="100%" style="max-width: 100%;">
|
||||
<p style="font-size: 14px; font-style: italic; color: #757575; text-align: center;">Wildlife in natural habitat</p>
|
||||
</div>
|
||||
<div style="flex: 1; padding: 10px; max-width: 33%;">
|
||||
<img src="https://images.unsplash.com/photo-1533038590840-1f12a7a36ddb" alt="Insect" width="100%" style="max-width: 100%;">
|
||||
<p style="font-size: 14px; font-style: italic; color: #757575; text-align: center;">Macro photography reveals tiny worlds</p>
|
||||
</div>
|
||||
<div style="flex: 1; padding: 10px; max-width: 33%;">
|
||||
<img src="https://images.unsplash.com/photo-1596328546171-77e37b5e8b3d" alt="Flowers" width="100%" style="max-width: 100%;">
|
||||
<p style="font-size: 14px; font-style: italic; color: #757575; text-align: center;">Vibrant flora adds color to our world</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<blockquote style="border-left: 4px solid #9c27b0; padding-left: 20px; margin-left: 0; font-style: italic; color: #555555;">
|
||||
<p>"In all things of nature there is something of the marvelous." — Aristotle</p>
|
||||
</blockquote>
|
||||
|
||||
<h2 style="font-size: 28px; color: #e91e63; margin-top: 30px; margin-bottom: 15px;">Conservation Efforts</h2>
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333333; margin-bottom: 20px;">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?</p>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<a href="#" style="display: inline-block; background-color: #4caf50; color: #ffffff; font-weight: bold; font-size: 16px; text-align: center; line-height: 1.5; border-radius: 4px; padding: 12px 24px; text-decoration: none; width: 200px;">Learn More</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #9e9e9e; margin-top: 40px; text-align: center;">Thank you for reading our blog post. Feel free to share it with others who might be interested in nature conservation.</p>`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
|
@ -141,10 +260,15 @@ const BlogEditPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (emailEditorRef.current) {
|
||||
emailEditorRef.current.editor.exportHtml(async (data) => {
|
||||
const { design, html } = data;
|
||||
|
||||
try {
|
||||
const formattedData = {
|
||||
...formData,
|
||||
// Convert tag objects to string names if needed
|
||||
content: html,
|
||||
design: design,
|
||||
tags: formData.tags.map(tag => typeof tag === 'string' ? tag : tag.name)
|
||||
};
|
||||
|
||||
|
|
@ -166,13 +290,25 @@ const BlogEditPage = () => {
|
|||
});
|
||||
setNotificationOpen(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Preview post
|
||||
const handlePreview = () => {
|
||||
// Store current form data in session storage
|
||||
sessionStorage.setItem('blog-post-preview', JSON.stringify(formData));
|
||||
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 = () => {
|
|||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Content */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Post Content
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
required
|
||||
multiline
|
||||
minRows={10}
|
||||
maxRows={20}
|
||||
name="content"
|
||||
value={formData.content}
|
||||
onChange={handleChange}
|
||||
placeholder="Write your blog post content here..."
|
||||
error={!!errors.content}
|
||||
helperText={errors.content}
|
||||
variant="outlined"
|
||||
/>
|
||||
<FormHelperText>
|
||||
Pro tip: You can use HTML markup for formatting
|
||||
</FormHelperText>
|
||||
</Grid>
|
||||
|
||||
{/* Excerpt */}
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
|
|
@ -434,13 +547,53 @@ const BlogEditPage = () => {
|
|||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Email Editor Tips */}
|
||||
<Grid item xs={12}>
|
||||
<Accordion sx={{ mb: 2 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InfoIcon sx={{ mr: 1 }} color="primary" />
|
||||
Editor Tips
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="subtitle1" gutterBottom>Tips for creating beautiful blog posts:</Typography>
|
||||
<ul>
|
||||
<li>Use the visual editor to create rich, visually appealing content</li>
|
||||
<li>You can drag and drop content blocks from the right panel</li>
|
||||
<li>The editor supports images, buttons, columns, and more</li>
|
||||
<li>For advanced users: You can switch to HTML mode to edit raw HTML</li>
|
||||
<li>Add your own images by using the image upload button in the editor</li>
|
||||
<li>Save often to avoid losing your work</li>
|
||||
</ul>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
For best results, design your blog post in Figma first, then implement it here using the visual tools.
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
|
||||
{/* Content (Email Editor) */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Post Content
|
||||
</Typography>
|
||||
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, height: '600px' }}>
|
||||
<EmailEditor
|
||||
ref={emailEditorRef}
|
||||
onReady={onEditorReady}
|
||||
minHeight="600px"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Actions */}
|
||||
<Grid item xs={12} sx={{ mt: 2, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<PreviewIcon />}
|
||||
onClick={handlePreview}
|
||||
disabled={!formData.title || !formData.content}
|
||||
disabled={!formData.title || !isEditorReady}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
|
|
@ -460,7 +613,7 @@ const BlogEditPage = () => {
|
|||
color="primary"
|
||||
startIcon={createPost.isLoading || updatePost.isLoading ?
|
||||
<CircularProgress size={20} /> : <SaveIcon />}
|
||||
disabled={createPost.isLoading || updatePost.isLoading}
|
||||
disabled={createPost.isLoading || updatePost.isLoading || !isEditorReady}
|
||||
>
|
||||
{createPost.isLoading || updatePost.isLoading ?
|
||||
'Saving...' : (isNewPost ? 'Create' : 'Update')} Post
|
||||
|
|
|
|||
236
frontend/src/pages/Admin/BlogPreviewPage.jsx
Normal file
236
frontend/src/pages/Admin/BlogPreviewPage.jsx
Normal file
|
|
@ -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 (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', my: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ my: 4 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// If post isn't found
|
||||
if (!post) {
|
||||
return (
|
||||
<Alert severity="warning" sx={{ my: 4 }}>
|
||||
Preview data not found. Please go back and try again.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ py: 4 }}>
|
||||
{/* Preview header */}
|
||||
<Paper sx={{ p: 2, mb: 3, bgcolor: 'info.light', color: 'info.contrastText' }}>
|
||||
<Typography variant="h6" component="div">
|
||||
Preview Mode
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
This is a preview of how your blog post will appear to readers. It is not published yet.
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
variant="contained"
|
||||
color="inherit"
|
||||
onClick={() => window.close()}
|
||||
>
|
||||
Close Preview
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<EditIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleEditClick}
|
||||
>
|
||||
Continue Editing
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
<Breadcrumbs
|
||||
separator={<NavigateNextIcon fontSize="small" />}
|
||||
aria-label="breadcrumb"
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
<Link component="span" color="inherit">
|
||||
Home
|
||||
</Link>
|
||||
<Link component="span" color="inherit">
|
||||
Blog
|
||||
</Link>
|
||||
<Typography color="text.primary">
|
||||
{post.title}
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
|
||||
{/* Post header */}
|
||||
<Paper sx={{ p: 3, mb: 4 }}>
|
||||
{/* Category */}
|
||||
{post.categoryId && (
|
||||
<Chip
|
||||
label="Category"
|
||||
color="primary"
|
||||
size="small"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
{post.title}
|
||||
</Typography>
|
||||
|
||||
{/* Author and date */}
|
||||
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||
Preview • {formatDate()}
|
||||
</Typography>
|
||||
|
||||
{/* Tags */}
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{post.tags && post.tags.filter(Boolean).map((tag, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={typeof tag === 'string' ? tag : tag.name}
|
||||
size="small"
|
||||
component="span"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Featured image */}
|
||||
{post.featuredImagePath && (
|
||||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||||
<img
|
||||
src={imageUtils.getImageUrl(post.featuredImagePath)}
|
||||
alt={post.title}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '500px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Post content */}
|
||||
<Paper sx={{ p: { xs: 2, md: 4 }, mb: 4 }}>
|
||||
<Box
|
||||
className="blog-content"
|
||||
sx={{
|
||||
'& p': { mb: 2 },
|
||||
'& h2': { mt: 3, mb: 2 },
|
||||
'& h3': { mt: 2.5, mb: 1.5 },
|
||||
'& ul, & ol': { mb: 2, pl: 4 },
|
||||
'& li': { mb: 0.5 },
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: '4px',
|
||||
my: 2
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
pl: 2,
|
||||
py: 1,
|
||||
my: 2,
|
||||
fontStyle: 'italic',
|
||||
bgcolor: 'background.paper'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Post content - rendered as HTML */}
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Comments section placeholder */}
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
Comments
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Comments will appear here when your post is published.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPreviewPage;
|
||||
|
|
@ -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 */}
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</Box>
|
||||
|
||||
{/* Post images */}
|
||||
{post.images && post.images.length > 0 && (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Gallery
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{post.images.map((image) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={image.id}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
pb: '75%', // 4:3 aspect ratio
|
||||
height: 0,
|
||||
overflow: 'hidden',
|
||||
borderRadius: 1
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageUtils.getImageUrl(image.path)}
|
||||
alt={image.caption || 'Blog image'}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{image.caption && (
|
||||
<Typography variant="caption" color="text.secondary" display="block" align="center" sx={{ mt: 1 }}>
|
||||
{image.caption}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Comments section */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue