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 { 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,
|
||||
|
||||
@@ -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 {
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<th>Transaction Date</th>
|
||||
@@ -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 <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(
|
||||
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 = `
|
||||
<div class="info-box">
|
||||
<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 style="text-align: center">
|
||||
<a href="${browseUrl}" class="button">Browse Other Items</a>
|
||||
@@ -1035,7 +1089,9 @@ class EmailService {
|
||||
<h2>Refund Information</h2>
|
||||
<div class="refund-amount">$${refundInfo.amount.toFixed(2)}</div>
|
||||
<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>Processing Time:</strong> Refunds typically appear within 5-10 business days.</p>
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<div class="warning-box">
|
||||
<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>
|
||||
<h2>Set Up Earnings to Get Paid</h2>
|
||||
<div class="info-box">
|
||||
@@ -1511,19 +1576,23 @@ class EmailService {
|
||||
stripeSection = `
|
||||
<div class="success-box">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="warning-box">
|
||||
<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>
|
||||
<h2>Set Up Earnings to Get Paid</h2>
|
||||
<div class="info-box">
|
||||
@@ -1733,7 +1807,9 @@ class EmailService {
|
||||
stripeSection = `
|
||||
<div class="success-box">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
@@ -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();
|
||||
|
||||
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