integrated email is forum posts/comments

This commit is contained in:
jackiettran
2025-11-14 15:51:25 -05:00
parent 105f257c5f
commit 629f0055a1
6 changed files with 1647 additions and 61 deletions

View File

@@ -4,6 +4,7 @@ const { ForumPost, ForumComment, PostTag, User } = require('../models');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload'); const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const emailService = require('../services/emailService');
const router = express.Router(); const router = express.Router();
// Helper function to build nested comment tree // Helper function to build nested comment tree
@@ -468,6 +469,46 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
status: commentId ? 'answered' : 'open' status: commentId ? 'answered' : 'open'
}); });
// Send email notification if marking an answer (not unmarking)
if (commentId) {
(async () => {
try {
const comment = await ForumComment.findByPk(commentId, {
include: [
{
model: User,
as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
}
]
});
const postAuthor = await User.findByPk(req.user.id, {
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
});
// Only send email if not marking your own comment as answer
if (comment && comment.authorId !== req.user.id) {
await emailService.sendForumAnswerAcceptedNotification(
comment.author,
postAuthor,
post,
comment
);
}
} catch (emailError) {
// Email errors don't block answer marking
logger.error("Failed to send answer accepted notification email", {
error: emailError.message,
stack: emailError.stack,
commentId: commentId,
postId: req.params.id
});
console.error("Email notification error:", emailError);
}
})();
}
const updatedPost = await ForumPost.findByPk(post.id, { const updatedPost = await ForumPost.findByPk(post.id, {
include: [ include: [
{ {
@@ -540,11 +581,107 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'username', 'firstName', 'lastName', 'email']
} }
] ]
}); });
// Send email notifications (non-blocking)
(async () => {
try {
const commenter = commentWithDetails.author;
const notifiedUserIds = new Set();
// Reload post with author details for email
const postWithAuthor = await ForumPost.findByPk(req.params.id, {
include: [
{
model: User,
as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
}
]
});
// If this is a reply, send reply notification to parent comment author
if (parentCommentId) {
const parentComment = await ForumComment.findByPk(parentCommentId, {
include: [
{
model: User,
as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
}
]
});
// Send reply notification if not replying to yourself
if (parentComment && parentComment.authorId !== req.user.id) {
await emailService.sendForumReplyNotification(
parentComment.author,
commenter,
postWithAuthor,
commentWithDetails,
parentComment
);
notifiedUserIds.add(parentComment.authorId);
}
} else {
// Send comment notification to post author if not commenting on your own post
if (postWithAuthor.authorId !== req.user.id) {
await emailService.sendForumCommentNotification(
postWithAuthor.author,
commenter,
postWithAuthor,
commentWithDetails
);
notifiedUserIds.add(postWithAuthor.authorId);
}
}
// Get all unique participants who have commented on this post (excluding commenter and already notified)
const participants = await ForumComment.findAll({
where: {
postId: req.params.id,
authorId: {
[Op.notIn]: [req.user.id, ...Array.from(notifiedUserIds)]
},
isDeleted: false
},
attributes: ['authorId'],
include: [
{
model: User,
as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email']
}
],
group: ['ForumComment.authorId', 'author.id']
});
// Send thread activity notifications to all unique participants
for (const participant of participants) {
if (participant.author) {
await emailService.sendForumThreadActivityNotification(
participant.author,
commenter,
postWithAuthor,
commentWithDetails
);
}
}
} catch (emailError) {
// Email errors don't block comment creation
logger.error("Failed to send forum comment notification emails", {
error: emailError.message,
stack: emailError.stack,
commentId: comment.id,
postId: req.params.id
});
console.error("Email notification error:", emailError);
}
})();
const reqLogger = logger.withRequestId(req.id); const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Forum comment created", { reqLogger.info("Forum comment created", {
postId: req.params.id, postId: req.params.id,

View File

@@ -55,6 +55,10 @@ class EmailService {
"feedbackConfirmationToUser.html", "feedbackConfirmationToUser.html",
"feedbackNotificationToAdmin.html", "feedbackNotificationToAdmin.html",
"newMessageToUser.html", "newMessageToUser.html",
"forumCommentToPostAuthor.html",
"forumReplyToCommentAuthor.html",
"forumAnswerAcceptedToCommentAuthor.html",
"forumThreadActivityToParticipant.html",
]; ];
for (const templateFile of templateFiles) { for (const templateFile of templateFiles) {
@@ -65,14 +69,23 @@ class EmailService {
this.templates.set(templateName, templateContent); this.templates.set(templateName, templateContent);
console.log(`✓ Loaded template: ${templateName}`); console.log(`✓ Loaded template: ${templateName}`);
} catch (error) { } catch (error) {
console.error(`✗ Failed to load template ${templateFile}:`, error.message); console.error(
console.error(` Template path: ${path.join(templatesDir, templateFile)}`); `✗ Failed to load template ${templateFile}:`,
error.message
);
console.error(
` Template path: ${path.join(templatesDir, templateFile)}`
);
} }
} }
console.log(`Loaded ${this.templates.size} of ${templateFiles.length} email templates`); console.log(
`Loaded ${this.templates.size} of ${templateFiles.length} email templates`
);
} catch (error) { } catch (error) {
console.warn("Templates directory not found, using fallback templates"); console.error("Failed to load email templates:", error);
console.error("Templates directory:", templatesDir);
console.error("Error stack:", error.stack);
} }
} }
@@ -178,19 +191,39 @@ class EmailService {
} }
} }
renderTemplate(templateName, variables = {}) { async renderTemplate(templateName, variables = {}) {
// Ensure service is initialized before rendering
if (!this.initialized) {
console.log(`Email service not initialized yet, initializing now...`);
await this.initialize();
}
let template = this.templates.get(templateName); let template = this.templates.get(templateName);
if (!template) { if (!template) {
console.error(`Template not found: ${templateName}`);
console.error(
`Available templates: ${Array.from(this.templates.keys()).join(", ")}`
);
console.error(`Stack trace:`, new Error().stack);
console.log(`Using fallback template for: ${templateName}`);
template = this.getFallbackTemplate(templateName); template = this.getFallbackTemplate(templateName);
} else {
console.log(`✓ Template found: ${templateName}`);
} }
let rendered = template; let rendered = template;
try {
Object.keys(variables).forEach((key) => { Object.keys(variables).forEach((key) => {
const regex = new RegExp(`{{${key}}}`, "g"); const regex = new RegExp(`{{${key}}}`, "g");
rendered = rendered.replace(regex, variables[key] || ""); rendered = rendered.replace(regex, variables[key] || "");
}); });
} catch (error) {
console.error(`Error rendering template ${templateName}:`, error);
console.error(`Stack trace:`, error.stack);
console.error(`Variables provided:`, Object.keys(variables));
}
return rendered; return rendered;
} }
@@ -504,7 +537,7 @@ class EmailService {
: "Not specified", : "Not specified",
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"conditionCheckReminderToUser", "conditionCheckReminderToUser",
variables variables
); );
@@ -543,13 +576,14 @@ class EmailService {
let paymentSection = ""; let paymentSection = "";
if (isRenter) { if (isRenter) {
const totalAmount = parseFloat(rental.totalAmount) || 0; const totalAmount = parseFloat(rental.totalAmount) || 0;
const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid'; const isPaidRental = totalAmount > 0 && rental.paymentStatus === "paid";
if (isPaidRental) { if (isPaidRental) {
// Format payment method display // Format payment method display
let paymentMethodDisplay = "Payment method on file"; let paymentMethodDisplay = "Payment method on file";
if (rental.paymentMethodBrand && rental.paymentMethodLast4) { if (rental.paymentMethodBrand && rental.paymentMethodLast4) {
const brandCapitalized = rental.paymentMethodBrand.charAt(0).toUpperCase() + const brandCapitalized =
rental.paymentMethodBrand.charAt(0).toUpperCase() +
rental.paymentMethodBrand.slice(1); rental.paymentMethodBrand.slice(1);
paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`; paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`;
} }
@@ -557,11 +591,11 @@ class EmailService {
const chargedAtFormatted = rental.chargedAt const chargedAtFormatted = rental.chargedAt
? new Date(rental.chargedAt).toLocaleString("en-US", { ? new Date(rental.chargedAt).toLocaleString("en-US", {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short" timeStyle: "short",
}) })
: new Date().toLocaleString("en-US", { : new Date().toLocaleString("en-US", {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short" timeStyle: "short",
}); });
// Build payment receipt section HTML // Build payment receipt section HTML
@@ -583,7 +617,9 @@ class EmailService {
</tr> </tr>
<tr> <tr>
<th>Transaction ID</th> <th>Transaction ID</th>
<td style="font-family: monospace; font-size: 12px;">${rental.stripePaymentIntentId || "N/A"}</td> <td style="font-family: monospace; font-size: 12px;">${
rental.stripePaymentIntentId || "N/A"
}</td>
</tr> </tr>
<tr> <tr>
<th>Transaction Date</th> <th>Transaction Date</th>
@@ -606,7 +642,10 @@ class EmailService {
variables.paymentSection = paymentSection; variables.paymentSection = paymentSection;
const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables); const htmlContent = await this.renderTemplate(
"rentalConfirmationToUser",
variables
);
// Use clear, transactional subject line with item name // Use clear, transactional subject line with item name
const subject = `Rental Confirmation - ${itemName}`; const subject = `Rental Confirmation - ${itemName}`;
@@ -623,7 +662,10 @@ class EmailService {
verificationUrl: verificationUrl, verificationUrl: verificationUrl,
}; };
const htmlContent = this.renderTemplate("emailVerificationToUser", variables); const htmlContent = await this.renderTemplate(
"emailVerificationToUser",
variables
);
return await this.sendEmail( return await this.sendEmail(
user.email, user.email,
@@ -633,11 +675,6 @@ class EmailService {
} }
async sendAlphaInvitation(email, code) { async sendAlphaInvitation(email, code) {
// Ensure service is initialized before rendering template
if (!this.initialized) {
await this.initialize();
}
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const variables = { const variables = {
@@ -648,7 +685,10 @@ class EmailService {
message: `You've been invited to join our exclusive alpha testing program. Use the code <strong>${code}</strong> to unlock access and be among the first to experience our platform.`, message: `You've been invited to join our exclusive alpha testing program. Use the code <strong>${code}</strong> to unlock access and be among the first to experience our platform.`,
}; };
const htmlContent = this.renderTemplate("alphaInvitationToUser", variables); const htmlContent = await this.renderTemplate(
"alphaInvitationToUser",
variables
);
return await this.sendEmail( return await this.sendEmail(
email, email,
@@ -666,7 +706,10 @@ class EmailService {
resetUrl: resetUrl, resetUrl: resetUrl,
}; };
const htmlContent = this.renderTemplate("passwordResetToUser", variables); const htmlContent = await this.renderTemplate(
"passwordResetToUser",
variables
);
return await this.sendEmail( return await this.sendEmail(
user.email, user.email,
@@ -687,7 +730,10 @@ class EmailService {
timestamp: timestamp, timestamp: timestamp,
}; };
const htmlContent = this.renderTemplate("passwordChangedToUser", variables); const htmlContent = await this.renderTemplate(
"passwordChangedToUser",
variables
);
return await this.sendEmail( return await this.sendEmail(
user.email, user.email,
@@ -744,7 +790,10 @@ class EmailService {
approveUrl: approveUrl, approveUrl: approveUrl,
}; };
const htmlContent = this.renderTemplate("rentalRequestToOwner", variables); const htmlContent = await this.renderTemplate(
"rentalRequestToOwner",
variables
);
return await this.sendEmail( return await this.sendEmail(
owner.email, owner.email,
@@ -797,7 +846,7 @@ class EmailService {
viewRentalsUrl: viewRentalsUrl, viewRentalsUrl: viewRentalsUrl,
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"rentalRequestConfirmationToRenter", "rentalRequestConfirmationToRenter",
variables variables
); );
@@ -819,9 +868,7 @@ class EmailService {
}); });
if (!renter) { if (!renter) {
console.error( console.error("Renter not found for rental decline notification");
"Renter not found for rental decline notification"
);
return { success: false, error: "Renter not found" }; return { success: false, error: "Renter not found" };
} }
@@ -867,7 +914,7 @@ class EmailService {
totalAmount: totalAmount.toFixed(2), totalAmount: totalAmount.toFixed(2),
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"rentalDeclinedToRenter", "rentalDeclinedToRenter",
variables variables
); );
@@ -920,11 +967,16 @@ class EmailService {
earningsDashboardUrl: earningsDashboardUrl, earningsDashboardUrl: earningsDashboardUrl,
}; };
const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables); const htmlContent = await this.renderTemplate(
"payoutReceivedToOwner",
variables
);
return await this.sendEmail( return await this.sendEmail(
owner.email, owner.email,
`Earnings Received - $${payoutAmount.toFixed(2)} for ${rental.item?.name || "Your Item"}`, `Earnings Received - $${payoutAmount.toFixed(2)} for ${
rental.item?.name || "Your Item"
}`,
htmlContent htmlContent
); );
} }
@@ -994,7 +1046,9 @@ class EmailService {
additionalInfo = ` additionalInfo = `
<div class="info-box"> <div class="info-box">
<p><strong>Full Refund Processed</strong></p> <p><strong>Full Refund Processed</strong></p>
<p>You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.</p> <p>You will receive a full refund of $${refundInfo.amount.toFixed(
2
)}. The refund will appear in your account within 5-10 business days.</p>
</div> </div>
<div style="text-align: center"> <div style="text-align: center">
<a href="${browseUrl}" class="button">Browse Other Items</a> <a href="${browseUrl}" class="button">Browse Other Items</a>
@@ -1035,7 +1089,9 @@ class EmailService {
<h2>Refund Information</h2> <h2>Refund Information</h2>
<div class="refund-amount">$${refundInfo.amount.toFixed(2)}</div> <div class="refund-amount">$${refundInfo.amount.toFixed(2)}</div>
<div class="info-box"> <div class="info-box">
<p><strong>Refund Amount:</strong> $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)</p> <p><strong>Refund Amount:</strong> $${refundInfo.amount.toFixed(
2
)} (${refundPercentage}% of total)</p>
<p><strong>Reason:</strong> ${refundInfo.reason}</p> <p><strong>Reason:</strong> ${refundInfo.reason}</p>
<p><strong>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p> <p><strong>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p>
</div> </div>
@@ -1063,7 +1119,7 @@ class EmailService {
refundSection: refundSection, refundSection: refundSection,
}; };
const confirmationHtml = this.renderTemplate( const confirmationHtml = await this.renderTemplate(
"rentalCancellationConfirmationToUser", "rentalCancellationConfirmationToUser",
confirmationVariables confirmationVariables
); );
@@ -1099,7 +1155,7 @@ class EmailService {
additionalInfo: additionalInfo, additionalInfo: additionalInfo,
}; };
const notificationHtml = this.renderTemplate( const notificationHtml = await this.renderTemplate(
"rentalCancellationNotificationToUser", "rentalCancellationNotificationToUser",
notificationVariables notificationVariables
); );
@@ -1112,7 +1168,9 @@ class EmailService {
if (notificationResult.success) { if (notificationResult.success) {
console.log( console.log(
`Cancellation notification email sent to ${cancelledBy === "owner" ? "renter" : "owner"}: ${notificationRecipient}` `Cancellation notification email sent to ${
cancelledBy === "owner" ? "renter" : "owner"
}: ${notificationRecipient}`
); );
results.notificationEmailSent = true; results.notificationEmailSent = true;
} }
@@ -1130,7 +1188,7 @@ class EmailService {
} }
async sendTemplateEmail(toEmail, subject, templateName, variables = {}) { async sendTemplateEmail(toEmail, subject, templateName, variables = {}) {
const htmlContent = this.renderTemplate(templateName, variables); const htmlContent = await this.renderTemplate(templateName, variables);
return await this.sendEmail(toEmail, subject, htmlContent); return await this.sendEmail(toEmail, subject, htmlContent);
} }
@@ -1413,7 +1471,7 @@ class EmailService {
viewItemUrl: `${frontendUrl}/items/${item.id}`, viewItemUrl: `${frontendUrl}/items/${item.id}`,
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"firstListingCelebrationToOwner", "firstListingCelebrationToOwner",
variables variables
); );
@@ -1428,7 +1486,12 @@ class EmailService {
// Fetch owner details // Fetch owner details
const owner = await User.findByPk(rental.ownerId, { const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"], attributes: [
"email",
"firstName",
"lastName",
"stripeConnectedAccountId",
],
}); });
// Fetch renter details // Fetch renter details
@@ -1487,7 +1550,9 @@ class EmailService {
stripeSection = ` stripeSection = `
<div class="warning-box"> <div class="warning-box">
<p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p> <p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p>
<p>To receive your payout of <strong>\$${payoutAmount.toFixed(2)}</strong> when this rental completes, you need to set up your earnings account.</p> <p>To receive your payout of <strong>\$${payoutAmount.toFixed(
2
)}</strong> when this rental completes, you need to set up your earnings account.</p>
</div> </div>
<h2>Set Up Earnings to Get Paid</h2> <h2>Set Up Earnings to Get Paid</h2>
<div class="info-box"> <div class="info-box">
@@ -1511,19 +1576,23 @@ class EmailService {
stripeSection = ` stripeSection = `
<div class="success-box"> <div class="success-box">
<p><strong>✓ Earnings Account Active</strong></p> <p><strong>✓ Earnings Account Active</strong></p>
<p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.</p> <p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(
2
)} when this rental completes.</p>
<p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p> <p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p>
</div> </div>
`; `;
} }
// Format delivery method for display // Format delivery method for display
const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup"; const deliveryMethodDisplay =
rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup";
const variables = { const variables = {
ownerName: owner.firstName || "there", ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item", itemName: rental.item?.name || "your item",
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter", renterName:
`${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: rental.startDateTime startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", { ? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium", dateStyle: "medium",
@@ -1543,7 +1612,7 @@ class EmailService {
rentalDetailsUrl: `${frontendUrl}/owning?rentalId=${rental.id}`, rentalDetailsUrl: `${frontendUrl}/owning?rentalId=${rental.id}`,
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"rentalApprovalConfirmationToOwner", "rentalApprovalConfirmationToOwner",
variables variables
); );
@@ -1563,7 +1632,12 @@ class EmailService {
try { try {
// Fetch owner details with Stripe info // Fetch owner details with Stripe info
const owner = await User.findByPk(rental.ownerId, { const owner = await User.findByPk(rental.ownerId, {
attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"], attributes: [
"email",
"firstName",
"lastName",
"stripeConnectedAccountId",
],
}); });
// Fetch renter details // Fetch renter details
@@ -1572,9 +1646,7 @@ class EmailService {
}); });
if (!owner || !renter) { if (!owner || !renter) {
console.error( console.error("Owner or renter not found for rental completion emails");
"Owner or renter not found for rental completion emails"
);
return { success: false, error: "User not found" }; return { success: false, error: "User not found" };
} }
@@ -1641,7 +1713,7 @@ class EmailService {
browseItemsUrl: `${frontendUrl}/`, browseItemsUrl: `${frontendUrl}/`,
}; };
const renterHtmlContent = this.renderTemplate( const renterHtmlContent = await this.renderTemplate(
"rentalCompletionThankYouToRenter", "rentalCompletionThankYouToRenter",
renterVariables renterVariables
); );
@@ -1709,7 +1781,9 @@ class EmailService {
stripeSection = ` stripeSection = `
<div class="warning-box"> <div class="warning-box">
<p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p> <p><strong>⚠️ Action Required: Set Up Your Earnings Account</strong></p>
<p>To receive your payout of <strong>\$${payoutAmount.toFixed(2)}</strong>, you need to set up your earnings account.</p> <p>To receive your payout of <strong>\$${payoutAmount.toFixed(
2
)}</strong>, you need to set up your earnings account.</p>
</div> </div>
<h2>Set Up Earnings to Get Paid</h2> <h2>Set Up Earnings to Get Paid</h2>
<div class="info-box"> <div class="info-box">
@@ -1733,7 +1807,9 @@ class EmailService {
stripeSection = ` stripeSection = `
<div class="success-box"> <div class="success-box">
<p><strong>✓ Earnings Account Active</strong></p> <p><strong>✓ Earnings Account Active</strong></p>
<p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.</p> <p>Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(
2
)} when the rental period ends.</p>
<p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p> <p><a href="${frontendUrl}/earnings" style="color: #155724; text-decoration: underline;">View your earnings dashboard →</a></p>
</div> </div>
`; `;
@@ -1744,7 +1820,8 @@ class EmailService {
const ownerVariables = { const ownerVariables = {
ownerName: owner.firstName || "there", ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item", itemName: rental.item?.name || "your item",
renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter", renterName:
`${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
returnedDate: returnedDate, returnedDate: returnedDate,
@@ -1753,7 +1830,7 @@ class EmailService {
owningUrl: `${frontendUrl}/owning`, owningUrl: `${frontendUrl}/owning`,
}; };
const ownerHtmlContent = this.renderTemplate( const ownerHtmlContent = await this.renderTemplate(
"rentalCompletionCongratsToOwner", "rentalCompletionCongratsToOwner",
ownerVariables ownerVariables
); );
@@ -1802,7 +1879,7 @@ class EmailService {
year: new Date().getFullYear(), year: new Date().getFullYear(),
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"feedbackConfirmationToUser", "feedbackConfirmationToUser",
variables variables
); );
@@ -1815,7 +1892,8 @@ class EmailService {
} }
async sendFeedbackNotificationToAdmin(user, feedback) { async sendFeedbackNotificationToAdmin(user, feedback) {
const adminEmail = process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL; const adminEmail =
process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
if (!adminEmail) { if (!adminEmail) {
console.warn("No admin email configured for feedback notifications"); console.warn("No admin email configured for feedback notifications");
@@ -1839,7 +1917,7 @@ class EmailService {
year: new Date().getFullYear(), year: new Date().getFullYear(),
}; };
const htmlContent = this.renderTemplate( const htmlContent = await this.renderTemplate(
"feedbackNotificationToAdmin", "feedbackNotificationToAdmin",
variables variables
); );
@@ -1870,7 +1948,10 @@ class EmailService {
timestamp: timestamp, timestamp: timestamp,
}; };
const htmlContent = this.renderTemplate("newMessageToUser", variables); const htmlContent = await this.renderTemplate(
"newMessageToUser",
variables
);
const subject = `New message from ${sender.firstName} ${sender.lastName}`; const subject = `New message from ${sender.firstName} ${sender.lastName}`;
@@ -1888,6 +1969,207 @@ class EmailService {
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }
async sendForumCommentNotification(postAuthor, commenter, post, comment) {
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
const variables = {
postAuthorName: postAuthor.firstName || "there",
commenterName:
`${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
postTitle: post.title,
commentContent: comment.content,
postUrl: postUrl,
timestamp: timestamp,
};
const htmlContent = await this.renderTemplate(
"forumCommentToPostAuthor",
variables
);
const subject = `${commenter.firstName} ${commenter.lastName} commented on your post`;
const result = await this.sendEmail(
postAuthor.email,
subject,
htmlContent
);
if (result.success) {
console.log(
`Forum comment notification email sent to ${postAuthor.email}`
);
}
return result;
} catch (error) {
console.error("Failed to send forum comment notification email:", error);
return { success: false, error: error.message };
}
}
async sendForumReplyNotification(
commentAuthor,
replier,
post,
reply,
parentComment
) {
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
const timestamp = new Date(reply.createdAt).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
const variables = {
commentAuthorName: commentAuthor.firstName || "there",
replierName:
`${replier.firstName} ${replier.lastName}`.trim() || "Someone",
postTitle: post.title,
parentCommentContent: parentComment.content,
replyContent: reply.content,
postUrl: postUrl,
timestamp: timestamp,
};
const htmlContent = await this.renderTemplate(
"forumReplyToCommentAuthor",
variables
);
const subject = `${replier.firstName} ${replier.lastName} replied to your comment`;
const result = await this.sendEmail(
commentAuthor.email,
subject,
htmlContent
);
if (result.success) {
console.log(
`Forum reply notification email sent to ${commentAuthor.email}`
);
}
return result;
} catch (error) {
console.error("Failed to send forum reply notification email:", error);
return { success: false, error: error.message };
}
}
async sendForumAnswerAcceptedNotification(
commentAuthor,
postAuthor,
post,
comment
) {
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
const variables = {
commentAuthorName: commentAuthor.firstName || "there",
postAuthorName:
`${postAuthor.firstName} ${postAuthor.lastName}`.trim() || "Someone",
postTitle: post.title,
commentContent: comment.content,
postUrl: postUrl,
};
const htmlContent = await this.renderTemplate(
"forumAnswerAcceptedToCommentAuthor",
variables
);
const subject = `Your comment was marked as the accepted answer!`;
const result = await this.sendEmail(
commentAuthor.email,
subject,
htmlContent
);
if (result.success) {
console.log(
`Forum answer accepted notification email sent to ${commentAuthor.email}`
);
}
return result;
} catch (error) {
console.error(
"Failed to send forum answer accepted notification email:",
error
);
return { success: false, error: error.message };
}
}
async sendForumThreadActivityNotification(
participant,
commenter,
post,
comment
) {
try {
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
const variables = {
participantName: participant.firstName || "there",
commenterName:
`${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
postTitle: post.title,
commentContent: comment.content,
postUrl: postUrl,
timestamp: timestamp,
};
const htmlContent = await this.renderTemplate(
"forumThreadActivityToParticipant",
variables
);
const subject = `New activity on a post you're following`;
const result = await this.sendEmail(
participant.email,
subject,
htmlContent
);
if (result.success) {
console.log(
`Forum thread activity notification email sent to ${participant.email}`
);
}
return result;
} catch (error) {
console.error(
"Failed to send forum thread activity notification email:",
error
);
return { success: false, error: error.message };
}
}
} }
module.exports = new EmailService(); module.exports = new EmailService();

View File

@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Your Comment Was Marked as the Answer</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #d4edda;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
}
/* Success box */
.success-box {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-left: 4px solid #28a745;
padding: 25px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
text-align: center;
}
.success-box .icon {
font-size: 48px;
margin-bottom: 10px;
}
.success-box h2 {
font-size: 20px;
font-weight: 600;
color: #155724;
margin: 0 0 10px 0;
}
.success-box p {
margin: 0;
color: #155724;
font-size: 15px;
}
/* Post title box */
.post-title-box {
background-color: #f8f9fa;
border-left: 4px solid #28a745;
padding: 15px 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.post-title-box .title {
font-size: 16px;
font-weight: 600;
color: #495057;
margin: 0;
}
/* Comment box */
.comment-box {
background-color: #f8f9fa;
border: 2px solid #28a745;
padding: 20px;
margin: 20px 0;
border-radius: 6px;
position: relative;
}
.comment-box .badge {
position: absolute;
top: -12px;
right: 20px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: #ffffff;
padding: 6px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.comment-box .content-text {
color: #212529;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Info box */
.info-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0;
color: #856404;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.success-box {
padding: 20px;
}
.success-box .icon {
font-size: 36px;
}
.comment-box {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Forum Recognition</div>
</div>
<div class="content">
<p>Hi {{commentAuthorName}},</p>
<h1>Your comment was marked as the accepted answer!</h1>
<div class="success-box">
<div class="icon"></div>
<h2>Great job helping the community!</h2>
<p>{{postAuthorName}} marked your comment as the accepted answer</p>
</div>
<p>Your helpful comment successfully answered this question:</p>
<div class="post-title-box">
<div class="title">{{postTitle}}</div>
</div>
<div class="comment-box">
<div class="badge">Accepted Answer</div>
<div class="content-text">{{commentContent}}</div>
</div>
<a href="{{postUrl}}" class="button">View Post</a>
<p>Thank you for contributing your knowledge and helping others in the RentAll community!</p>
<div class="info-box">
<p><strong>Keep it up!</strong> Your contributions make RentAll a better place for everyone. Continue sharing your expertise and helping fellow community members.</p>
</div>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>You received this email because your forum comment was marked as the accepted answer.</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>New Comment on Your Post</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #d4edda;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0;
color: #004085;
}
/* Post title box */
.post-title-box {
background-color: #f8f9fa;
border-left: 4px solid #28a745;
padding: 15px 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.post-title-box .title {
font-size: 16px;
font-weight: 600;
color: #495057;
margin: 0;
}
/* Comment box */
.comment-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 20px;
margin: 20px 0;
border-radius: 6px;
}
.comment-box .author {
font-size: 14px;
font-weight: 600;
color: #495057;
margin: 0 0 10px 0;
}
.comment-box .content-text {
color: #212529;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.comment-box .timestamp {
font-size: 12px;
color: #6c757d;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.comment-box {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Forum Activity</div>
</div>
<div class="content">
<p>Hi {{postAuthorName}},</p>
<h1>{{commenterName}} commented on your post</h1>
<p>Someone just commented on your forum post:</p>
<div class="post-title-box">
<div class="title">{{postTitle}}</div>
</div>
<div class="comment-box">
<div class="author">{{commenterName}}</div>
<div class="content-text">{{commentContent}}</div>
<div class="timestamp">Posted {{timestamp}}</div>
</div>
<a href="{{postUrl}}" class="button">View Post & Reply</a>
<p>Click the button above to see the full discussion and respond to this comment.</p>
<div class="info-box">
<p><strong>Tip:</strong> Engaging with commenters helps build a vibrant community and provides better answers for everyone.</p>
</div>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>You received this email because someone commented on your forum post.</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,303 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>New Reply to Your Comment</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #d4edda;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0;
color: #004085;
}
/* Post title box */
.post-title-box {
background-color: #f8f9fa;
border-left: 4px solid #28a745;
padding: 15px 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.post-title-box .title {
font-size: 16px;
font-weight: 600;
color: #495057;
margin: 0;
}
/* Comment boxes */
.your-comment-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 15px 20px;
margin: 20px 0 10px 0;
border-radius: 6px;
}
.your-comment-box .label {
font-size: 12px;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 8px 0;
}
.your-comment-box .content-text {
color: #6c757d;
font-size: 14px;
line-height: 1.5;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.reply-box {
background-color: #ffffff;
border: 2px solid #28a745;
padding: 20px;
margin: 0 0 20px 30px;
border-radius: 6px;
}
.reply-box .author {
font-size: 14px;
font-weight: 600;
color: #495057;
margin: 0 0 10px 0;
}
.reply-box .content-text {
color: #212529;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.reply-box .timestamp {
font-size: 12px;
color: #6c757d;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.reply-box {
margin: 0 0 20px 15px;
padding: 15px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Forum Activity</div>
</div>
<div class="content">
<p>Hi {{commentAuthorName}},</p>
<h1>{{replierName}} replied to your comment</h1>
<p>Someone just replied to your comment in the forum:</p>
<div class="post-title-box">
<div class="title">{{postTitle}}</div>
</div>
<div class="your-comment-box">
<div class="label">Your Comment</div>
<div class="content-text">{{parentCommentContent}}</div>
</div>
<div class="reply-box">
<div class="author">{{replierName}}</div>
<div class="content-text">{{replyContent}}</div>
<div class="timestamp">Posted {{timestamp}}</div>
</div>
<a href="{{postUrl}}" class="button">View Reply & Respond</a>
<p>Click the button above to see the full discussion and continue the conversation.</p>
<div class="info-box">
<p><strong>Tip:</strong> Thoughtful replies help create meaningful discussions and build community connections.</p>
</div>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>You received this email because someone replied to your forum comment.</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>New Activity on a Forum Post You Follow</title>
<style>
/* Reset styles */
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* Base styles */
body {
margin: 0;
padding: 0;
width: 100% !important;
min-width: 100%;
height: 100%;
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #212529;
}
/* Container */
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Header */
.header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
padding: 40px 30px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 700;
color: #ffffff;
text-decoration: none;
letter-spacing: -1px;
}
.tagline {
color: #d4edda;
font-size: 14px;
margin-top: 8px;
}
/* Content */
.content {
padding: 40px 30px;
}
.content h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
color: #212529;
}
.content h2 {
font-size: 20px;
font-weight: 600;
margin: 30px 0 15px 0;
color: #495057;
}
.content p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Button */
.button {
display: inline-block;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: #ffffff !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 6px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
}
/* Info box */
.info-box {
background-color: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.info-box p {
margin: 0;
color: #004085;
}
/* Post title box */
.post-title-box {
background-color: #f8f9fa;
border-left: 4px solid #28a745;
padding: 15px 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.post-title-box .label {
font-size: 12px;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 8px 0;
}
.post-title-box .title {
font-size: 16px;
font-weight: 600;
color: #495057;
margin: 0;
}
/* Comment box */
.comment-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 20px;
margin: 20px 0;
border-radius: 6px;
}
.comment-box .author {
font-size: 14px;
font-weight: 600;
color: #495057;
margin: 0 0 10px 0;
}
.comment-box .content-text {
color: #212529;
line-height: 1.6;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.comment-box .timestamp {
font-size: 12px;
color: #6c757d;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
/* Footer */
.footer {
background-color: #f8f9fa;
padding: 30px;
text-align: center;
border-top: 1px solid #e9ecef;
}
.footer p {
margin: 0 0 10px 0;
font-size: 14px;
color: #6c757d;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media only screen and (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.header, .content, .footer {
padding: 20px;
}
.logo {
font-size: 28px;
}
.content h1 {
font-size: 22px;
}
.button {
display: block;
width: 100%;
box-sizing: border-box;
}
.comment-box {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Forum Activity</div>
</div>
<div class="content">
<p>Hi {{participantName}},</p>
<h1>New activity on a post you're following</h1>
<p>{{commenterName}} just commented on a forum post you've participated in:</p>
<div class="post-title-box">
<div class="label">Post You're Following</div>
<div class="title">{{postTitle}}</div>
</div>
<div class="comment-box">
<div class="author">{{commenterName}}</div>
<div class="content-text">{{commentContent}}</div>
<div class="timestamp">Posted {{timestamp}}</div>
</div>
<a href="{{postUrl}}" class="button">View Discussion</a>
<p>Click the button above to see the full conversation and join the discussion.</p>
<div class="info-box">
<p><strong>Stay engaged:</strong> You're receiving this because you've commented on this post. Keep the conversation going!</p>
</div>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>You received this email because there's new activity on a forum post you've commented on.</p>
<p>If you have any questions, please contact our support team.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>