From 629f0055a12c8c1d4e20775da58e403036d3c7b4 Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:51:25 -0500 Subject: [PATCH] integrated email is forum posts/comments --- backend/routes/forum.js | 139 +++++- backend/services/emailService.js | 402 +++++++++++++++--- .../forumAnswerAcceptedToCommentAuthor.html | 312 ++++++++++++++ .../emails/forumCommentToPostAuthor.html | 271 ++++++++++++ .../emails/forumReplyToCommentAuthor.html | 303 +++++++++++++ .../forumThreadActivityToParticipant.html | 281 ++++++++++++ 6 files changed, 1647 insertions(+), 61 deletions(-) create mode 100644 backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html create mode 100644 backend/templates/emails/forumCommentToPostAuthor.html create mode 100644 backend/templates/emails/forumReplyToCommentAuthor.html create mode 100644 backend/templates/emails/forumThreadActivityToParticipant.html diff --git a/backend/routes/forum.js b/backend/routes/forum.js index d212c0a..4d5f385 100644 --- a/backend/routes/forum.js +++ b/backend/routes/forum.js @@ -4,6 +4,7 @@ const { ForumPost, ForumComment, PostTag, User } = require('../models'); const { authenticateToken } = require('../middleware/auth'); const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload'); const logger = require('../utils/logger'); +const emailService = require('../services/emailService'); const router = express.Router(); // 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' }); + // 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, { include: [ { @@ -540,11 +581,107 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages, { model: User, 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); reqLogger.info("Forum comment created", { postId: req.params.id, diff --git a/backend/services/emailService.js b/backend/services/emailService.js index 441f0d4..1ddac48 100644 --- a/backend/services/emailService.js +++ b/backend/services/emailService.js @@ -55,6 +55,10 @@ class EmailService { "feedbackConfirmationToUser.html", "feedbackNotificationToAdmin.html", "newMessageToUser.html", + "forumCommentToPostAuthor.html", + "forumReplyToCommentAuthor.html", + "forumAnswerAcceptedToCommentAuthor.html", + "forumThreadActivityToParticipant.html", ]; for (const templateFile of templateFiles) { @@ -65,14 +69,23 @@ class EmailService { this.templates.set(templateName, templateContent); console.log(`✓ Loaded template: ${templateName}`); } catch (error) { - console.error(`✗ Failed to load template ${templateFile}:`, error.message); - console.error(` Template path: ${path.join(templatesDir, templateFile)}`); + console.error( + `✗ 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) { - 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); 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); + } else { + console.log(`✓ Template found: ${templateName}`); } let rendered = template; - Object.keys(variables).forEach((key) => { - const regex = new RegExp(`{{${key}}}`, "g"); - rendered = rendered.replace(regex, variables[key] || ""); - }); + try { + Object.keys(variables).forEach((key) => { + const regex = new RegExp(`{{${key}}}`, "g"); + 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; } @@ -504,7 +537,7 @@ class EmailService { : "Not specified", }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "conditionCheckReminderToUser", variables ); @@ -543,25 +576,26 @@ class EmailService { let paymentSection = ""; if (isRenter) { const totalAmount = parseFloat(rental.totalAmount) || 0; - const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid'; + const isPaidRental = totalAmount > 0 && rental.paymentStatus === "paid"; if (isPaidRental) { // Format payment method display let paymentMethodDisplay = "Payment method on file"; if (rental.paymentMethodBrand && rental.paymentMethodLast4) { - const brandCapitalized = rental.paymentMethodBrand.charAt(0).toUpperCase() + - rental.paymentMethodBrand.slice(1); + const brandCapitalized = + rental.paymentMethodBrand.charAt(0).toUpperCase() + + rental.paymentMethodBrand.slice(1); paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`; } const chargedAtFormatted = rental.chargedAt ? new Date(rental.chargedAt).toLocaleString("en-US", { dateStyle: "medium", - timeStyle: "short" + timeStyle: "short", }) : new Date().toLocaleString("en-US", { dateStyle: "medium", - timeStyle: "short" + timeStyle: "short", }); // Build payment receipt section HTML @@ -583,7 +617,9 @@ class EmailService { Transaction ID - ${rental.stripePaymentIntentId || "N/A"} + ${ + rental.stripePaymentIntentId || "N/A" + } Transaction Date @@ -606,7 +642,10 @@ class EmailService { variables.paymentSection = paymentSection; - const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables); + const htmlContent = await this.renderTemplate( + "rentalConfirmationToUser", + variables + ); // Use clear, transactional subject line with item name const subject = `Rental Confirmation - ${itemName}`; @@ -623,7 +662,10 @@ class EmailService { verificationUrl: verificationUrl, }; - const htmlContent = this.renderTemplate("emailVerificationToUser", variables); + const htmlContent = await this.renderTemplate( + "emailVerificationToUser", + variables + ); return await this.sendEmail( user.email, @@ -633,11 +675,6 @@ class EmailService { } 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 variables = { @@ -648,7 +685,10 @@ class EmailService { message: `You've been invited to join our exclusive alpha testing program. Use the code ${code} 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( email, @@ -666,7 +706,10 @@ class EmailService { resetUrl: resetUrl, }; - const htmlContent = this.renderTemplate("passwordResetToUser", variables); + const htmlContent = await this.renderTemplate( + "passwordResetToUser", + variables + ); return await this.sendEmail( user.email, @@ -687,7 +730,10 @@ class EmailService { timestamp: timestamp, }; - const htmlContent = this.renderTemplate("passwordChangedToUser", variables); + const htmlContent = await this.renderTemplate( + "passwordChangedToUser", + variables + ); return await this.sendEmail( user.email, @@ -744,7 +790,10 @@ class EmailService { approveUrl: approveUrl, }; - const htmlContent = this.renderTemplate("rentalRequestToOwner", variables); + const htmlContent = await this.renderTemplate( + "rentalRequestToOwner", + variables + ); return await this.sendEmail( owner.email, @@ -797,7 +846,7 @@ class EmailService { viewRentalsUrl: viewRentalsUrl, }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "rentalRequestConfirmationToRenter", variables ); @@ -819,9 +868,7 @@ class EmailService { }); if (!renter) { - console.error( - "Renter not found for rental decline notification" - ); + console.error("Renter not found for rental decline notification"); return { success: false, error: "Renter not found" }; } @@ -867,7 +914,7 @@ class EmailService { totalAmount: totalAmount.toFixed(2), }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "rentalDeclinedToRenter", variables ); @@ -920,11 +967,16 @@ class EmailService { earningsDashboardUrl: earningsDashboardUrl, }; - const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables); + const htmlContent = await this.renderTemplate( + "payoutReceivedToOwner", + variables + ); return await this.sendEmail( 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 ); } @@ -994,7 +1046,9 @@ class EmailService { additionalInfo = `

Full Refund Processed

-

You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.

+

You will receive a full refund of $${refundInfo.amount.toFixed( + 2 + )}. The refund will appear in your account within 5-10 business days.

Browse Other Items @@ -1035,7 +1089,9 @@ class EmailService {

Refund Information

$${refundInfo.amount.toFixed(2)}
-

Refund Amount: $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)

+

Refund Amount: $${refundInfo.amount.toFixed( + 2 + )} (${refundPercentage}% of total)

Reason: ${refundInfo.reason}

Processing Time: Refunds typically appear within 5-10 business days.

@@ -1063,7 +1119,7 @@ class EmailService { refundSection: refundSection, }; - const confirmationHtml = this.renderTemplate( + const confirmationHtml = await this.renderTemplate( "rentalCancellationConfirmationToUser", confirmationVariables ); @@ -1099,7 +1155,7 @@ class EmailService { additionalInfo: additionalInfo, }; - const notificationHtml = this.renderTemplate( + const notificationHtml = await this.renderTemplate( "rentalCancellationNotificationToUser", notificationVariables ); @@ -1112,7 +1168,9 @@ class EmailService { if (notificationResult.success) { 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; } @@ -1130,7 +1188,7 @@ class EmailService { } 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); } @@ -1413,7 +1471,7 @@ class EmailService { viewItemUrl: `${frontendUrl}/items/${item.id}`, }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "firstListingCelebrationToOwner", variables ); @@ -1428,7 +1486,12 @@ class EmailService { // Fetch owner details const owner = await User.findByPk(rental.ownerId, { - attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"], + attributes: [ + "email", + "firstName", + "lastName", + "stripeConnectedAccountId", + ], }); // Fetch renter details @@ -1487,7 +1550,9 @@ class EmailService { stripeSection = `

⚠️ Action Required: Set Up Your Earnings Account

-

To receive your payout of \$${payoutAmount.toFixed(2)} when this rental completes, you need to set up your earnings account.

+

To receive your payout of \$${payoutAmount.toFixed( + 2 + )} when this rental completes, you need to set up your earnings account.

Set Up Earnings to Get Paid

@@ -1511,19 +1576,23 @@ class EmailService { stripeSection = `

✓ Earnings Account Active

-

Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.

+

Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed( + 2 + )} when this rental completes.

View your earnings dashboard →

`; } // Format delivery method for display - const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup"; + const deliveryMethodDisplay = + rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup"; const variables = { ownerName: owner.firstName || "there", 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 ? new Date(rental.startDateTime).toLocaleString("en-US", { dateStyle: "medium", @@ -1543,7 +1612,7 @@ class EmailService { rentalDetailsUrl: `${frontendUrl}/owning?rentalId=${rental.id}`, }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "rentalApprovalConfirmationToOwner", variables ); @@ -1563,7 +1632,12 @@ class EmailService { try { // Fetch owner details with Stripe info const owner = await User.findByPk(rental.ownerId, { - attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"], + attributes: [ + "email", + "firstName", + "lastName", + "stripeConnectedAccountId", + ], }); // Fetch renter details @@ -1572,9 +1646,7 @@ class EmailService { }); if (!owner || !renter) { - console.error( - "Owner or renter not found for rental completion emails" - ); + console.error("Owner or renter not found for rental completion emails"); return { success: false, error: "User not found" }; } @@ -1641,7 +1713,7 @@ class EmailService { browseItemsUrl: `${frontendUrl}/`, }; - const renterHtmlContent = this.renderTemplate( + const renterHtmlContent = await this.renderTemplate( "rentalCompletionThankYouToRenter", renterVariables ); @@ -1709,7 +1781,9 @@ class EmailService { stripeSection = `

⚠️ Action Required: Set Up Your Earnings Account

-

To receive your payout of \$${payoutAmount.toFixed(2)}, you need to set up your earnings account.

+

To receive your payout of \$${payoutAmount.toFixed( + 2 + )}, you need to set up your earnings account.

Set Up Earnings to Get Paid

@@ -1733,7 +1807,9 @@ class EmailService { stripeSection = `

✓ Earnings Account Active

-

Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.

+

Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed( + 2 + )} when the rental period ends.

View your earnings dashboard →

`; @@ -1744,7 +1820,8 @@ class EmailService { const ownerVariables = { ownerName: owner.firstName || "there", itemName: rental.item?.name || "your item", - renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter", + renterName: + `${renter.firstName} ${renter.lastName}`.trim() || "The renter", startDate: startDate, endDate: endDate, returnedDate: returnedDate, @@ -1753,7 +1830,7 @@ class EmailService { owningUrl: `${frontendUrl}/owning`, }; - const ownerHtmlContent = this.renderTemplate( + const ownerHtmlContent = await this.renderTemplate( "rentalCompletionCongratsToOwner", ownerVariables ); @@ -1802,7 +1879,7 @@ class EmailService { year: new Date().getFullYear(), }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "feedbackConfirmationToUser", variables ); @@ -1815,7 +1892,8 @@ class EmailService { } 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) { console.warn("No admin email configured for feedback notifications"); @@ -1839,7 +1917,7 @@ class EmailService { year: new Date().getFullYear(), }; - const htmlContent = this.renderTemplate( + const htmlContent = await this.renderTemplate( "feedbackNotificationToAdmin", variables ); @@ -1870,7 +1948,10 @@ class EmailService { timestamp: timestamp, }; - const htmlContent = this.renderTemplate("newMessageToUser", variables); + const htmlContent = await this.renderTemplate( + "newMessageToUser", + variables + ); const subject = `New message from ${sender.firstName} ${sender.lastName}`; @@ -1888,6 +1969,207 @@ class EmailService { 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(); diff --git a/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html b/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html new file mode 100644 index 0000000..fc28f06 --- /dev/null +++ b/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html @@ -0,0 +1,312 @@ + + + + + + + Your Comment Was Marked as the Answer + + + + + + diff --git a/backend/templates/emails/forumCommentToPostAuthor.html b/backend/templates/emails/forumCommentToPostAuthor.html new file mode 100644 index 0000000..56ddeeb --- /dev/null +++ b/backend/templates/emails/forumCommentToPostAuthor.html @@ -0,0 +1,271 @@ + + + + + + + New Comment on Your Post + + + + + + diff --git a/backend/templates/emails/forumReplyToCommentAuthor.html b/backend/templates/emails/forumReplyToCommentAuthor.html new file mode 100644 index 0000000..e0baf20 --- /dev/null +++ b/backend/templates/emails/forumReplyToCommentAuthor.html @@ -0,0 +1,303 @@ + + + + + + + New Reply to Your Comment + + + + + + diff --git a/backend/templates/emails/forumThreadActivityToParticipant.html b/backend/templates/emails/forumThreadActivityToParticipant.html new file mode 100644 index 0000000..32514b8 --- /dev/null +++ b/backend/templates/emails/forumThreadActivityToParticipant.html @@ -0,0 +1,281 @@ + + + + + + + New Activity on a Forum Post You Follow + + + + + +