integrated email is forum posts/comments
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
312
backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html
Normal file
312
backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
271
backend/templates/emails/forumCommentToPostAuthor.html
Normal file
271
backend/templates/emails/forumCommentToPostAuthor.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
303
backend/templates/emails/forumReplyToCommentAuthor.html
Normal file
303
backend/templates/emails/forumReplyToCommentAuthor.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
281
backend/templates/emails/forumThreadActivityToParticipant.html
Normal file
281
backend/templates/emails/forumThreadActivityToParticipant.html
Normal 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>© 2024 RentAll. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user