email sent when personal information changed

This commit is contained in:
jackiettran
2025-11-21 16:47:39 -05:00
parent f7767dfd13
commit f2d42dffee
9 changed files with 701 additions and 153 deletions

View File

@@ -1,54 +1,54 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require("sequelize");
const sequelize = require('../config/database'); const sequelize = require("../config/database");
const UserAddress = sequelize.define('UserAddress', { const UserAddress = sequelize.define("UserAddress", {
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
primaryKey: true primaryKey: true,
}, },
userId: { userId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: false,
references: { references: {
model: 'Users', model: "Users",
key: 'id' key: "id",
} },
}, },
address1: { address1: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
address2: { address2: {
type: DataTypes.STRING type: DataTypes.STRING,
}, },
city: { city: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
state: { state: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
zipCode: { zipCode: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
country: { country: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'US' defaultValue: "US",
}, },
latitude: { latitude: {
type: DataTypes.DECIMAL(10, 8) type: DataTypes.DECIMAL(10, 8),
}, },
longitude: { longitude: {
type: DataTypes.DECIMAL(11, 8) type: DataTypes.DECIMAL(11, 8),
}, },
isPrimary: { isPrimary: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: false defaultValue: false,
} },
}); });
module.exports = UserAddress; module.exports = UserAddress;

View File

@@ -3,6 +3,7 @@ const { User, UserAddress } = require('../models'); // Import from models/index.
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { uploadProfileImage } = require('../middleware/upload'); const { uploadProfileImage } = require('../middleware/upload');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const userService = require('../services/UserService');
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
const router = express.Router(); const router = express.Router();
@@ -57,15 +58,7 @@ router.get('/addresses', authenticateToken, async (req, res) => {
router.post('/addresses', authenticateToken, async (req, res) => { router.post('/addresses', authenticateToken, async (req, res) => {
try { try {
const address = await UserAddress.create({ const address = await userService.createUserAddress(req.user.id, req.body);
...req.body,
userId: req.user.id
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User address created", {
userId: req.user.id,
addressId: address.id
});
res.status(201).json(address); res.status(201).json(address);
} catch (error) { } catch (error) {
@@ -82,23 +75,7 @@ router.post('/addresses', authenticateToken, async (req, res) => {
router.put('/addresses/:id', authenticateToken, async (req, res) => { router.put('/addresses/:id', authenticateToken, async (req, res) => {
try { try {
const address = await UserAddress.findByPk(req.params.id); const address = await userService.updateUserAddress(req.user.id, req.params.id, req.body);
if (!address) {
return res.status(404).json({ error: 'Address not found' });
}
if (address.userId !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
await address.update(req.body);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User address updated", {
userId: req.user.id,
addressId: req.params.id
});
res.json(address); res.json(address);
} catch (error) { } catch (error) {
@@ -109,29 +86,18 @@ router.put('/addresses/:id', authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
addressId: req.params.id addressId: req.params.id
}); });
if (error.message === 'Address not found') {
return res.status(404).json({ error: error.message });
}
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
router.delete('/addresses/:id', authenticateToken, async (req, res) => { router.delete('/addresses/:id', authenticateToken, async (req, res) => {
try { try {
const address = await UserAddress.findByPk(req.params.id); await userService.deleteUserAddress(req.user.id, req.params.id);
if (!address) {
return res.status(404).json({ error: 'Address not found' });
}
if (address.userId !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
await address.destroy();
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User address deleted", {
userId: req.user.id,
addressId: req.params.id
});
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
@@ -142,6 +108,11 @@ router.delete('/addresses/:id', authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
addressId: req.params.id addressId: req.params.id
}); });
if (error.message === 'Address not found') {
return res.status(404).json({ error: error.message });
}
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
@@ -211,44 +182,8 @@ router.get('/:id', async (req, res) => {
router.put('/profile', authenticateToken, async (req, res) => { router.put('/profile', authenticateToken, async (req, res) => {
try { try {
const { // Use UserService to handle update and email notification
firstName, const updatedUser = await userService.updateProfile(req.user.id, req.body);
lastName,
email,
phone,
address1,
address2,
city,
state,
zipCode,
country,
itemRequestNotificationRadius
} = req.body;
// Build update object, excluding empty email
const updateData = {
firstName,
lastName,
phone,
address1,
address2,
city,
state,
zipCode,
country,
itemRequestNotificationRadius
};
// Only include email if it's not empty
if (email && email.trim() !== '') {
updateData.email = email;
}
await req.user.update(updateData);
const updatedUser = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] }
});
res.json(updatedUser); res.json(updatedUser);
} catch (error) { } catch (error) {

View File

@@ -32,6 +32,7 @@ const feedbackRoutes = require("./routes/feedback");
const PayoutProcessor = require("./jobs/payoutProcessor"); const PayoutProcessor = require("./jobs/payoutProcessor");
const RentalStatusJob = require("./jobs/rentalStatusJob"); const RentalStatusJob = require("./jobs/rentalStatusJob");
const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder"); const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder");
const emailServices = require("./services/email");
// Socket.io setup // Socket.io setup
const { authenticateSocket } = require("./sockets/socketAuth"); const { authenticateSocket } = require("./sockets/socketAuth");
@@ -166,9 +167,27 @@ const PORT = process.env.PORT || 5000;
sequelize sequelize
.sync({ alter: true }) .sync({ alter: true })
.then(() => { .then(async () => {
logger.info("Database synced successfully"); logger.info("Database synced successfully");
// Initialize email services and load templates
try {
await emailServices.initialize();
logger.info("Email services initialized successfully");
} catch (err) {
logger.error("Failed to initialize email services", {
error: err.message,
stack: err.stack,
});
// Fail fast - don't start server if email templates can't load
if (env === "prod" || env === "production") {
logger.error("Cannot start server without email services in production");
process.exit(1);
} else {
logger.warn("Email services failed to initialize - continuing in dev mode");
}
}
// Start the payout processor // Start the payout processor
const payoutJobs = PayoutProcessor.startScheduledPayouts(); const payoutJobs = PayoutProcessor.startScheduledPayouts();
logger.info("Payout processor started"); logger.info("Payout processor started");

View File

@@ -0,0 +1,234 @@
const { User, UserAddress } = require("../models");
const emailServices = require("./email");
const logger = require("../utils/logger");
/**
* UserService handles user-related business logic
* Including profile updates and associated notifications
*/
class UserService {
/**
* Update user profile and send notification if personal info changed
* @param {string} userId - User ID
* @param {Object} rawUpdateData - Data to update
* @param {Object} options - Optional transaction or other options
* @returns {Promise<User>} Updated user (without password field)
*/
async updateProfile(userId, rawUpdateData, options = {}) {
const user = await User.findByPk(userId);
if (!user) {
throw new Error("User not found");
}
// Store original values for comparison
const originalValues = {
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
address1: user.address1,
address2: user.address2,
city: user.city,
state: user.state,
zipCode: user.zipCode,
country: user.country,
};
// Prepare update data with preprocessing
const updateData = { ...rawUpdateData };
// Only include email if it's not empty
if (updateData.email !== undefined) {
if (updateData.email && updateData.email.trim() !== "") {
updateData.email = updateData.email.trim();
} else {
delete updateData.email; // Don't update if empty
}
}
// Handle phone: convert empty strings to null to avoid unique constraint issues
if (updateData.phone !== undefined) {
updateData.phone =
updateData.phone && updateData.phone.trim() !== ""
? updateData.phone.trim()
: null;
}
// Perform the update
await user.update(updateData, options);
// Check if personal information changed
const personalInfoFields = [
"email",
"firstName",
"lastName",
"address1",
"address2",
"city",
"state",
"zipCode",
"country",
];
const changedFields = personalInfoFields.filter(
(field) =>
updateData[field] !== undefined &&
originalValues[field] !== updateData[field]
);
// Send notification email if personal info changed
if (changedFields.length > 0 && process.env.NODE_ENV !== "test") {
try {
await emailServices.auth.sendPersonalInfoChangedEmail(user);
logger.info("Personal information changed notification sent", {
userId: user.id,
email: user.email,
changedFields,
});
} catch (emailError) {
logger.error(
"Failed to send personal information changed notification",
{
error: emailError.message,
userId: user.id,
email: user.email,
changedFields,
}
);
// Don't throw - email failure shouldn't fail the update
}
}
// Return user without password
const updatedUser = await User.findByPk(user.id, {
attributes: { exclude: ["password"] },
});
return updatedUser;
}
/**
* Create a new address for a user and send notification
* @param {string} userId - User ID
* @param {Object} addressData - Address data
* @returns {Promise<UserAddress>} Created address
*/
async createUserAddress(userId, addressData) {
const user = await User.findByPk(userId);
if (!user) {
throw new Error("User not found");
}
const address = await UserAddress.create({
...addressData,
userId,
});
// Send notification for address creation
if (process.env.NODE_ENV !== "test") {
try {
await emailServices.auth.sendPersonalInfoChangedEmail(user);
logger.info(
"Personal information changed notification sent (address created)",
{
userId: user.id,
email: user.email,
addressId: address.id,
}
);
} catch (emailError) {
logger.error("Failed to send notification for address creation", {
error: emailError.message,
userId: user.id,
addressId: address.id,
});
}
}
return address;
}
/**
* Update a user address and send notification
* @param {string} userId - User ID
* @param {string} addressId - Address ID
* @param {Object} updateData - Data to update
* @returns {Promise<UserAddress>} Updated address
*/
async updateUserAddress(userId, addressId, updateData) {
const address = await UserAddress.findOne({
where: { id: addressId, userId },
});
if (!address) {
throw new Error("Address not found");
}
await address.update(updateData);
// Send notification for address update
if (process.env.NODE_ENV !== "test") {
try {
const user = await User.findByPk(userId);
await emailServices.auth.sendPersonalInfoChangedEmail(user);
logger.info(
"Personal information changed notification sent (address updated)",
{
userId: user.id,
email: user.email,
addressId: address.id,
}
);
} catch (emailError) {
logger.error("Failed to send notification for address update", {
error: emailError.message,
userId,
addressId: address.id,
});
}
}
return address;
}
/**
* Delete a user address and send notification
* @param {string} userId - User ID
* @param {string} addressId - Address ID
* @returns {Promise<void>}
*/
async deleteUserAddress(userId, addressId) {
const address = await UserAddress.findOne({
where: { id: addressId, userId },
});
if (!address) {
throw new Error("Address not found");
}
await address.destroy();
// Send notification for address deletion
if (process.env.NODE_ENV !== "test") {
try {
const user = await User.findByPk(userId);
await emailServices.auth.sendPersonalInfoChangedEmail(user);
logger.info(
"Personal information changed notification sent (address deleted)",
{
userId: user.id,
email: user.email,
addressId,
}
);
} catch (emailError) {
logger.error("Failed to send notification for address deletion", {
error: emailError.message,
userId,
addressId,
});
}
}
}
}
module.exports = new UserService();

View File

@@ -11,8 +11,16 @@ const { htmlToPlainText } = require("./emailUtils");
*/ */
class EmailClient { class EmailClient {
constructor() { constructor() {
// Singleton pattern - return existing instance if already created
if (EmailClient.instance) {
return EmailClient.instance;
}
this.sesClient = null; this.sesClient = null;
this.initialized = false; this.initialized = false;
this.initializationPromise = null;
EmailClient.instance = this;
} }
/** /**
@@ -20,19 +28,30 @@ class EmailClient {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initialize() { async initialize() {
// If already initialized, return immediately
if (this.initialized) return; if (this.initialized) return;
try { // If initialization is in progress, wait for it
// Use centralized AWS configuration with credential profiles if (this.initializationPromise) {
const awsConfig = getAWSConfig(); return this.initializationPromise;
this.sesClient = new SESClient(awsConfig);
this.initialized = true;
console.log("AWS SES Email Client initialized successfully");
} catch (error) {
console.error("Failed to initialize AWS SES Email Client:", error);
throw error;
} }
// Start initialization and store the promise
this.initializationPromise = (async () => {
try {
// Use centralized AWS configuration with credential profiles
const awsConfig = getAWSConfig();
this.sesClient = new SESClient(awsConfig);
this.initialized = true;
console.log("AWS SES Email Client initialized successfully");
} catch (error) {
console.error("Failed to initialize AWS SES Email Client:", error);
throw error;
}
})();
return this.initializationPromise;
} }
/** /**

View File

@@ -11,8 +11,16 @@ const path = require("path");
*/ */
class TemplateManager { class TemplateManager {
constructor() { constructor() {
// Singleton pattern - return existing instance if already created
if (TemplateManager.instance) {
return TemplateManager.instance;
}
this.templates = new Map(); this.templates = new Map();
this.initialized = false; this.initialized = false;
this.initializationPromise = null;
TemplateManager.instance = this;
} }
/** /**
@@ -20,11 +28,22 @@ class TemplateManager {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initialize() { async initialize() {
// If already initialized, return immediately
if (this.initialized) return; if (this.initialized) return;
await this.loadEmailTemplates(); // If initialization is in progress, wait for it
this.initialized = true; if (this.initializationPromise) {
console.log("Email Template Manager initialized successfully"); return this.initializationPromise;
}
// Start initialization and store the promise
this.initializationPromise = (async () => {
await this.loadEmailTemplates();
this.initialized = true;
console.log("Email Template Manager initialized successfully");
})();
return this.initializationPromise;
} }
/** /**
@@ -34,6 +53,14 @@ class TemplateManager {
async loadEmailTemplates() { async loadEmailTemplates() {
const templatesDir = path.join(__dirname, "..", "..", "..", "templates", "emails"); const templatesDir = path.join(__dirname, "..", "..", "..", "templates", "emails");
// Critical templates that must load for the app to function
const criticalTemplates = [
"emailVerificationToUser.html",
"passwordResetToUser.html",
"passwordChangedToUser.html",
"personalInfoChangedToUser.html",
];
try { try {
const templateFiles = [ const templateFiles = [
"conditionCheckReminderToUser.html", "conditionCheckReminderToUser.html",
@@ -41,6 +68,7 @@ class TemplateManager {
"emailVerificationToUser.html", "emailVerificationToUser.html",
"passwordResetToUser.html", "passwordResetToUser.html",
"passwordChangedToUser.html", "passwordChangedToUser.html",
"personalInfoChangedToUser.html",
"lateReturnToCS.html", "lateReturnToCS.html",
"damageReportToCS.html", "damageReportToCS.html",
"lostItemToCS.html", "lostItemToCS.html",
@@ -69,6 +97,8 @@ class TemplateManager {
"forumCommentDeletionToAuthor.html", "forumCommentDeletionToAuthor.html",
]; ];
const failedTemplates = [];
for (const templateFile of templateFiles) { for (const templateFile of templateFiles) {
try { try {
const templatePath = path.join(templatesDir, templateFile); const templatePath = path.join(templatesDir, templateFile);
@@ -84,16 +114,39 @@ class TemplateManager {
console.error( console.error(
` Template path: ${path.join(templatesDir, templateFile)}` ` Template path: ${path.join(templatesDir, templateFile)}`
); );
failedTemplates.push(templateFile);
} }
} }
console.log( console.log(
`Loaded ${this.templates.size} of ${templateFiles.length} email templates` `Loaded ${this.templates.size} of ${templateFiles.length} email templates`
); );
// Check if critical templates are missing
const missingCriticalTemplates = criticalTemplates.filter(
(template) => !this.templates.has(path.basename(template, ".html"))
);
if (missingCriticalTemplates.length > 0) {
const error = new Error(
`Critical email templates failed to load: ${missingCriticalTemplates.join(", ")}`
);
error.missingTemplates = missingCriticalTemplates;
throw error;
}
// Warn if non-critical templates failed
if (failedTemplates.length > 0) {
console.warn(
`⚠️ Non-critical templates failed to load: ${failedTemplates.join(", ")}`
);
console.warn("These templates will use fallback versions");
}
} catch (error) { } catch (error) {
console.error("Failed to load email templates:", error); console.error("Failed to load email templates:", error);
console.error("Templates directory:", templatesDir); console.error("Templates directory:", templatesDir);
console.error("Error stack:", error.stack); console.error("Error stack:", error.stack);
throw error; // Re-throw to fail server startup
} }
} }

View File

@@ -131,6 +131,41 @@ class AuthEmailService {
htmlContent htmlContent
); );
} }
/**
* Send personal information changed notification email
* @param {Object} user - User object
* @param {string} user.firstName - User's first name
* @param {string} user.email - User's email address
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendPersonalInfoChangedEmail(user) {
if (!this.initialized) {
await this.initialize();
}
const timestamp = new Date().toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
});
const variables = {
recipientName: user.firstName || "there",
email: user.email,
timestamp: timestamp,
};
const htmlContent = await this.templateManager.renderTemplate(
"personalInfoChangedToUser",
variables
);
return await this.emailClient.sendEmail(
user.email,
"Personal Information Updated - RentAll",
htmlContent
);
}
} }
module.exports = AuthEmailService; module.exports = AuthEmailService;

View File

@@ -0,0 +1,246 @@
<!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>Personal Information Updated - RentAll</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 p {
margin: 0 0 16px 0;
color: #6c757d;
line-height: 1.6;
}
.content strong {
color: #495057;
}
/* Success box */
.success-box {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.success-box p {
margin: 0;
color: #155724;
font-size: 14px;
}
/* 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;
font-size: 14px;
}
/* Security box */
.security-box {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 15px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.security-box p {
margin: 0;
color: #721c24;
font-size: 14px;
}
/* Details table */
.details-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.details-table td {
padding: 12px;
border-bottom: 1px solid #e9ecef;
}
.details-table td:first-child {
font-weight: 600;
color: #495057;
width: 40%;
}
.details-table td:last-child {
color: #6c757d;
}
/* 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;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo">RentAll</div>
<div class="tagline">Personal Information Updated</div>
</div>
<div class="content">
<p>Hi {{recipientName}},</p>
<h1>Your Personal Information Has Been Updated</h1>
<div class="info-box">
<p><strong>Your account information was recently updated.</strong> This email is to notify you that changes were made to your personal information on your RentAll account.</p>
</div>
<p>We're sending you this notification as part of our commitment to keeping your account secure. If you made these changes, no further action is required.</p>
<table class="details-table">
<tr>
<td>Date & Time:</td>
<td>{{timestamp}}</td>
</tr>
<tr>
<td>Account Email:</td>
<td>{{email}}</td>
</tr>
</table>
<div class="security-box">
<p><strong>Didn't make these changes?</strong> If you did not update your personal information, your account may be compromised. Please contact our support team immediately at support@rentall.com and consider changing your password.</p>
</div>
<div class="info-box">
<p><strong>Security tip:</strong> Regularly review your account information to ensure it's accurate and up to date. If you notice any suspicious activity, contact our support team right away.</p>
</div>
<p>Thanks for using RentAll!</p>
</div>
<div class="footer">
<p><strong>RentAll</strong></p>
<p>This is a security notification sent to confirm changes to your account. If you have any concerns about your account security, please contact our support team immediately.</p>
<p>&copy; 2024 RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -807,20 +807,52 @@ const Profile: React.FC = () => {
{/* Personal Information Card */} {/* Personal Information Card */}
<div className="card mb-4"> <div className="card mb-4">
<div className="card-body"> <div className="card-body">
<div className="d-flex align-items-center mb-3"> <div className="d-flex align-items-center justify-content-between mb-3">
<h5 className="card-title mb-0">Personal Information</h5> <div className="d-flex align-items-center">
<button <h5 className="card-title mb-0">Personal Information</h5>
type="button" <button
className="btn btn-link text-primary p-0 ms-2" type="button"
onClick={() => setShowPersonalInfo(!showPersonalInfo)} className="btn btn-link text-primary p-0 ms-2"
style={{ textDecoration: "none" }} onClick={() => setShowPersonalInfo(!showPersonalInfo)}
> style={{ textDecoration: "none" }}
<i >
className={`bi ${ <i
showPersonalInfo ? "bi-eye" : "bi-eye-slash" className={`bi ${
} fs-5`} showPersonalInfo ? "bi-eye" : "bi-eye-slash"
></i> } fs-5`}
</button> ></i>
</button>
</div>
{showPersonalInfo && (
<div>
{editing ? (
<div className="d-flex gap-2">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={handleSubmit}
>
Save Changes
</button>
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={handleCancel}
>
Cancel
</button>
</div>
) : (
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => setEditing(true)}
>
Edit Information
</button>
)}
</div>
)}
</div> </div>
{showPersonalInfo && ( {showPersonalInfo && (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -1125,31 +1157,6 @@ const Profile: React.FC = () => {
</> </>
)} )}
</div> </div>
<hr className="my-4" />
{editing ? (
<div className="d-flex gap-2">
<button type="submit" className="btn btn-primary">
Save Changes
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancel}
>
Cancel
</button>
</div>
) : (
<button
type="button"
className="btn btn-primary"
onClick={() => setEditing(true)}
>
Edit Information
</button>
)}
</form> </form>
)} )}
</div> </div>