Compare commits

...

2 Commits

Author SHA1 Message Date
jackiettran
16272ba373 feedback tab 2025-10-31 16:48:18 -04:00
jackiettran
99aa0b3bdc badge when owner gets pending rental request 2025-10-31 12:18:40 -04:00
17 changed files with 907 additions and 4 deletions

View File

@@ -310,6 +310,22 @@ const validateVerifyResetToken = [
handleValidationErrors,
];
// Feedback validation
const validateFeedback = [
body("feedbackText")
.trim()
.isLength({ min: 5, max: 5000 })
.withMessage("Feedback must be between 5 and 5000 characters"),
body("url")
.optional()
.trim()
.isLength({ max: 500 })
.withMessage("URL must be less than 500 characters"),
handleValidationErrors,
];
module.exports = {
sanitizeInput,
handleValidationErrors,
@@ -321,4 +337,5 @@ module.exports = {
validateForgotPassword,
validateResetPassword,
validateVerifyResetToken,
validateFeedback,
};

View File

@@ -0,0 +1,34 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Feedback = sequelize.define('Feedback', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
feedbackText: {
type: DataTypes.TEXT,
allowNull: false
},
userAgent: {
type: DataTypes.STRING,
allowNull: true
},
url: {
type: DataTypes.STRING(500),
allowNull: true
}
}, {
timestamps: true
});
module.exports = Feedback;

View File

@@ -8,6 +8,7 @@ const ItemRequestResponse = require("./ItemRequestResponse");
const UserAddress = require("./UserAddress");
const ConditionCheck = require("./ConditionCheck");
const AlphaInvitation = require("./AlphaInvitation");
const Feedback = require("./Feedback");
User.hasMany(Item, { as: "ownedItems", foreignKey: "ownerId" });
Item.belongsTo(User, { as: "owner", foreignKey: "ownerId" });
@@ -82,6 +83,10 @@ User.hasMany(AlphaInvitation, {
foreignKey: "usedBy",
});
// Feedback associations
User.hasMany(Feedback, { as: "feedbacks", foreignKey: "userId" });
Feedback.belongsTo(User, { as: "user", foreignKey: "userId" });
module.exports = {
sequelize,
User,
@@ -93,4 +98,5 @@ module.exports = {
UserAddress,
ConditionCheck,
AlphaInvitation,
Feedback,
};

View File

@@ -0,0 +1,66 @@
const express = require('express');
const { Feedback, User } = require('../models');
const { authenticateToken } = require('../middleware/auth');
const { validateFeedback, sanitizeInput } = require('../middleware/validation');
const logger = require('../utils/logger');
const emailService = require('../services/emailService');
const router = express.Router();
// Submit new feedback
router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res) => {
try {
const { feedbackText, url } = req.body;
// Capture user agent from request headers
const userAgent = req.headers['user-agent'];
const feedback = await Feedback.create({
userId: req.user.id,
feedbackText,
url: url || null,
userAgent: userAgent || null
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Feedback submitted", {
userId: req.user.id,
feedbackId: feedback.id
});
// Send confirmation email to user
try {
await emailService.sendFeedbackConfirmation(req.user, feedback);
} catch (emailError) {
reqLogger.error("Failed to send feedback confirmation email", {
error: emailError.message,
userId: req.user.id,
feedbackId: feedback.id
});
// Don't fail the request if email fails
}
// Send notification email to admin
try {
await emailService.sendFeedbackNotificationToAdmin(req.user, feedback);
} catch (emailError) {
reqLogger.error("Failed to send feedback notification to admin", {
error: emailError.message,
userId: req.user.id,
feedbackId: feedback.id
});
// Don't fail the request if email fails
}
res.status(201).json(feedback);
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Feedback submission failed", {
error: error.message,
stack: error.stack,
userId: req.user.id
});
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -109,6 +109,28 @@ router.get("/my-listings", authenticateToken, async (req, res) => {
}
});
// Get count of pending rental requests for owner
router.get("/pending-requests-count", authenticateToken, async (req, res) => {
try {
const count = await Rental.count({
where: {
ownerId: req.user.id,
status: "pending",
},
});
res.json({ count });
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Error getting pending rental count", {
error: error.message,
stack: error.stack,
userId: req.user.id,
});
res.status(500).json({ error: "Failed to get pending rental count" });
}
});
// Get rental by ID
router.get("/:id", authenticateToken, async (req, res) => {
try {

View File

@@ -25,6 +25,7 @@ const itemRequestRoutes = require("./routes/itemRequests");
const stripeRoutes = require("./routes/stripe");
const mapsRoutes = require("./routes/maps");
const conditionCheckRoutes = require("./routes/conditionChecks");
const feedbackRoutes = require("./routes/feedback");
const PayoutProcessor = require("./jobs/payoutProcessor");
const RentalStatusJob = require("./jobs/rentalStatusJob");
@@ -126,6 +127,7 @@ app.use("/api/item-requests", requireAlphaAccess, itemRequestRoutes);
app.use("/api/stripe", requireAlphaAccess, stripeRoutes);
app.use("/api/maps", requireAlphaAccess, mapsRoutes);
app.use("/api/condition-checks", requireAlphaAccess, conditionCheckRoutes);
app.use("/api/feedback", requireAlphaAccess, feedbackRoutes);
// Error handling middleware (must be last)
app.use(errorLogger);

View File

@@ -52,6 +52,8 @@ class EmailService {
"payoutReceivedToOwner.html",
"firstListingCelebrationToOwner.html",
"alphaInvitationToUser.html",
"feedbackConfirmationToUser.html",
"feedbackNotificationToAdmin.html",
];
for (const templateFile of templateFiles) {
@@ -443,6 +445,40 @@ class EmailService {
<p><a href="{{myListingsUrl}}" class="button">View My Listings</a></p>
`
),
feedbackConfirmationToUser: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{userName}},</p>
<h2>Thank You for Your Feedback!</h2>
<p>We've received your feedback and our team will review it carefully.</p>
<div style="background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; font-style: italic;">
{{feedbackText}}
</div>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<p>Your input helps us improve RentAll for everyone. We take all feedback seriously and use it to make the platform better.</p>
<p>If your feedback requires a response, our team will reach out to you directly.</p>
`
),
feedbackNotificationToAdmin: baseTemplate.replace(
"{{content}}",
`
<h2>New Feedback Received</h2>
<p><strong>From:</strong> {{userName}} ({{userEmail}})</p>
<p><strong>User ID:</strong> {{userId}}</p>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<h3>Feedback Content</h3>
<div style="background-color: #e7f3ff; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0;">
{{feedbackText}}
</div>
<h3>Technical Context</h3>
<p><strong>Feedback ID:</strong> {{feedbackId}}</p>
<p><strong>Page URL:</strong> {{url}}</p>
<p><strong>User Agent:</strong> {{userAgent}}</p>
<p>Please review this feedback and take appropriate action if needed.</p>
`
),
};
return (
@@ -1750,6 +1786,69 @@ class EmailService {
return results;
}
async sendFeedbackConfirmation(user, feedback) {
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
});
const variables = {
userName: user.firstName || "there",
userEmail: user.email,
feedbackText: feedback.feedbackText,
submittedAt: submittedAt,
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
"feedbackConfirmationToUser",
variables
);
return await this.sendEmail(
user.email,
"Thank You for Your Feedback - RentAll",
htmlContent
);
}
async sendFeedbackNotificationToAdmin(user, feedback) {
const adminEmail = process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
if (!adminEmail) {
console.warn("No admin email configured for feedback notifications");
return { success: false, error: "No admin email configured" };
}
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
});
const variables = {
userName: `${user.firstName} ${user.lastName}`.trim() || "Unknown User",
userEmail: user.email,
userId: user.id,
feedbackText: feedback.feedbackText,
feedbackId: feedback.id,
url: feedback.url || "Not provided",
userAgent: feedback.userAgent || "Not provided",
submittedAt: submittedAt,
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
"feedbackNotificationToAdmin",
variables
);
return await this.sendEmail(
adminEmail,
`New Feedback from ${user.firstName} ${user.lastName}`,
htmlContent
);
}
}
module.exports = new EmailService();

View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedback Received - RentAll</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
border-bottom: 2px solid #e9ecef;
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
}
.content {
line-height: 1.6;
color: #555;
}
.success-box {
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.success-box .icon {
font-size: 32px;
text-align: center;
margin-bottom: 10px;
}
.success-box p {
margin: 5px 0;
color: #155724;
}
.feedback-preview {
background-color: #f8f9fa;
border-left: 4px solid #007bff;
padding: 15px;
margin: 20px 0;
font-style: italic;
color: #495057;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
font-size: 12px;
color: #6c757d;
}
.button {
display: inline-block;
background-color: #007bff;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
h2 {
color: #333;
margin-top: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">RentAll</div>
</div>
<div class="content">
<p>Hi {{userName}},</p>
<div class="success-box">
<div class="icon"></div>
<p><strong>Thank You for Your Feedback!</strong></p>
<p>We've received your feedback and our team will review it carefully.</p>
</div>
<h2>Your Feedback</h2>
<div class="feedback-preview">
{{feedbackText}}
</div>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<p>Your input helps us improve RentAll for everyone. We take all feedback seriously and use it to make the platform better.</p>
<p>If your feedback requires a response, our team will reach out to you directly at <strong>{{userEmail}}</strong>.</p>
<p>Want to share more thoughts? Feel free to send us additional feedback anytime through the app.</p>
</div>
<div class="footer">
<p>This email was sent from RentAll. If you have any questions, please contact support.</p>
<p>&copy; {{year}} RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Feedback Received - RentAll</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 700px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
border-bottom: 2px solid #e9ecef;
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
}
.alert-box {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.alert-box p {
margin: 5px 0;
color: #856404;
}
.content {
line-height: 1.6;
color: #555;
}
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.info-table th {
background-color: #f8f9fa;
padding: 12px;
text-align: left;
border: 1px solid #dee2e6;
font-weight: bold;
color: #495057;
width: 30%;
}
.info-table td {
padding: 12px;
border: 1px solid #dee2e6;
color: #212529;
word-break: break-word;
}
.feedback-box {
background-color: #e7f3ff;
border-left: 4px solid #007bff;
padding: 20px;
margin: 20px 0;
font-size: 14px;
line-height: 1.6;
color: #004085;
}
.feedback-box h3 {
margin-top: 0;
color: #004085;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
font-size: 12px;
color: #6c757d;
}
h2 {
color: #333;
margin-top: 0;
}
.timestamp {
color: #6c757d;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">RentAll Admin</div>
</div>
<div class="content">
<div class="alert-box">
<p><strong>New Feedback Received</strong></p>
<p class="timestamp">Submitted at: {{submittedAt}}</p>
</div>
<h2>User Information</h2>
<table class="info-table">
<tr>
<th>Name</th>
<td>{{userName}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{userEmail}}</td>
</tr>
<tr>
<th>User ID</th>
<td style="font-family: monospace; font-size: 12px;">{{userId}}</td>
</tr>
</table>
<div class="feedback-box">
<h3>Feedback Content</h3>
<p>{{feedbackText}}</p>
</div>
<h2>Technical Context</h2>
<table class="info-table">
<tr>
<th>Feedback ID</th>
<td style="font-family: monospace; font-size: 12px;">{{feedbackId}}</td>
</tr>
<tr>
<th>Page URL</th>
<td>{{url}}</td>
</tr>
<tr>
<th>User Agent</th>
<td style="font-size: 12px;">{{userAgent}}</td>
</tr>
<tr>
<th>Submitted At</th>
<td>{{submittedAt}}</td>
</tr>
</table>
<p><strong>Action Required:</strong> Please review this feedback and take appropriate action. If a response is needed, contact the user directly at <strong>{{userEmail}}</strong>.</p>
</div>
<div class="footer">
<p>This is an automated notification from RentAll Feedback System</p>
<p>&copy; {{year}} RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -5,6 +5,7 @@ import Navbar from './components/Navbar';
import Footer from './components/Footer';
import AuthModal from './components/AuthModal';
import AlphaGate from './components/AlphaGate';
import FeedbackButton from './components/FeedbackButton';
import Home from './pages/Home';
import GoogleCallback from './pages/GoogleCallback';
import VerifyEmail from './pages/VerifyEmail';
@@ -32,7 +33,7 @@ import './App.css';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001';
const AppContent: React.FC = () => {
const { showAuthModal, authModalMode, closeAuthModal } = useAuth();
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
const [hasAlphaAccess, setHasAlphaAccess] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
@@ -192,6 +193,9 @@ const AppContent: React.FC = () => {
onHide={closeAuthModal}
initialMode={authModalMode}
/>
{/* Show feedback button for authenticated users */}
{user && <FeedbackButton />}
</>
);
};

View File

@@ -51,6 +51,9 @@ const DeclineRentalModal: React.FC<DeclineRentalModalProps> = ({
// Call parent callback with updated rental data if we have it
if (updatedRental) {
onDeclineComplete(updatedRental);
// Notify Navbar to update pending count
window.dispatchEvent(new CustomEvent("rentalStatusChanged"));
}
// Reset all states when closing

View File

@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import FeedbackModal from './FeedbackModal';
const FeedbackButton: React.FC = () => {
const [showPanel, setShowPanel] = useState(false);
return (
<>
<button
className="feedback-tab"
onClick={() => setShowPanel(true)}
title="Send Feedback"
aria-label="Send Feedback"
>
<div className="feedback-tab-text">FEEDBACK</div>
</button>
<FeedbackModal show={showPanel} onClose={() => setShowPanel(false)} />
<style>{`
.feedback-tab {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 8px 0 0 8px;
padding: 16px 10px;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
writing-mode: vertical-rl;
text-orientation: mixed;
}
.feedback-tab:hover {
background-color: #0b5ed7;
padding-right: 14px;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
}
.feedback-tab:active {
background-color: #0a58ca;
}
.feedback-tab-text {
font-size: 14px;
font-weight: 600;
letter-spacing: 2px;
margin: 0;
transform: rotate(180deg);
}
@media (max-width: 768px) {
.feedback-tab {
padding: 12px 8px;
}
.feedback-tab-text {
font-size: 12px;
letter-spacing: 1px;
}
}
`}</style>
</>
);
};
export default FeedbackButton;

View File

@@ -0,0 +1,217 @@
import React, { useState, useEffect } from "react";
import { feedbackAPI } from "../services/api";
interface FeedbackModalProps {
show: boolean;
onClose: () => void;
}
const FeedbackModal: React.FC<FeedbackModalProps> = ({ show, onClose }) => {
const [feedbackText, setFeedbackText] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const MIN_LENGTH = 5;
const MAX_LENGTH = 5000;
const charCount = feedbackText.length;
const isValid = charCount >= MIN_LENGTH && charCount <= MAX_LENGTH;
useEffect(() => {
if (!show) {
// Reset form when modal closes
setTimeout(() => {
setFeedbackText("");
setError("");
setSuccess(false);
}, 300);
}
}, [show]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isValid) {
setError(
`Feedback must be between ${MIN_LENGTH} and ${MAX_LENGTH} characters`
);
return;
}
setLoading(true);
setError("");
try {
// Capture the current URL
const currentUrl = window.location.href;
await feedbackAPI.submitFeedback({
feedbackText: feedbackText.trim(),
url: currentUrl,
});
setSuccess(true);
setLoading(false);
} catch (err: any) {
setError(
err.response?.data?.error ||
"Failed to submit feedback. Please try again."
);
setLoading(false);
}
};
if (!show) return null;
return (
<>
<div className={`feedback-panel ${show ? "show" : ""}`}>
<div className="feedback-panel-content">
<div className="modal-header">
<h5 className="modal-title">Share Feedback</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
disabled={loading}
></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{success ? (
<div className="alert alert-success" role="alert">
<h6 className="alert-heading">Thank you!</h6>
<p className="mb-0">
Your feedback has been submitted successfully! We appreciate
you making Community Rentals better!
</p>
</div>
) : (
<>
<p className="text-muted mb-3">
Share your thoughts, report bugs, or suggest improvements.
Your feedback helps us make RentAll better for everyone!
</p>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="mb-3">
<textarea
id="feedbackText"
className="form-control"
rows={6}
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
disabled={loading}
maxLength={MAX_LENGTH}
required
/>
{charCount > 0 && charCount < MIN_LENGTH && (
<div
className="text-danger mt-2"
style={{ fontSize: "0.875rem" }}
>
Minimum {MIN_LENGTH} characters required
</div>
)}
</div>
</>
)}
</div>
{!success && (
<div className="modal-footer" style={{ gap: "10px" }}>
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
disabled={loading || !isValid}
>
{loading ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Sending...
</>
) : (
"Share"
)}
</button>
</div>
)}
</form>
</div>
</div>
<style>{`
.feedback-panel {
position: fixed;
top: 50%;
right: -400px;
transform: translateY(-50%);
height: auto;
max-height: calc(100vh - 40px);
width: 400px;
background-color: white;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
border-radius: 8px 0 0 8px;
z-index: 1050;
transition: right 0.3s ease;
overflow-y: auto;
}
.feedback-panel.show {
right: 0;
}
.feedback-panel-content {
height: 100%;
display: flex;
flex-direction: column;
}
.feedback-panel .modal-header {
border-bottom: none;
padding: 1rem 1.5rem 0.5rem 1.5rem;
flex-shrink: 0;
}
.feedback-panel .modal-body {
flex: 1;
padding: 0.5rem 1.5rem 1.5rem 1.5rem;
overflow-y: auto;
}
.feedback-panel .modal-footer {
border-top: none;
padding: 0 1.5rem 1rem 1.5rem;
flex-shrink: 0;
}
@media (max-width: 576px) {
.feedback-panel {
width: 100%;
right: -100%;
}
}
`}</style>
</>
);
};
export default FeedbackModal;

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { rentalAPI } from "../services/api";
const Navbar: React.FC = () => {
const { user, logout, openAuthModal } = useAuth();
@@ -9,6 +10,36 @@ const Navbar: React.FC = () => {
search: "",
location: "",
});
const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
// Fetch pending rental requests count when user logs in
useEffect(() => {
const fetchPendingCount = async () => {
if (user) {
try {
const response = await rentalAPI.getPendingRequestsCount();
setPendingRequestsCount(response.data.count);
} catch (error) {
console.error("Failed to fetch pending requests count:", error);
}
} else {
setPendingRequestsCount(0);
}
};
fetchPendingCount();
// Listen for rental status changes to refresh count
const handleRentalStatusChange = () => {
fetchPendingCount();
};
window.addEventListener("rentalStatusChanged", handleRentalStatusChange);
return () => {
window.removeEventListener("rentalStatusChanged", handleRentalStatusChange);
};
}, [user]);
const handleLogout = () => {
logout();
@@ -123,8 +154,35 @@ const Navbar: React.FC = () => {
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span style={{ display: "flex", alignItems: "center", position: "relative" }}>
{pendingRequestsCount > 0 && (
<span
style={{
position: "absolute",
left: "-0.9em",
top: "50%",
transform: "translateY(-50%)",
backgroundColor: "#dc3545",
color: "white",
borderRadius: "50%",
width: "1.5em",
height: "1.5em",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.85em",
fontWeight: "bold",
border: "2px solid white",
opacity: 1,
zIndex: 1,
}}
>
{pendingRequestsCount}
</span>
)}
<i className="bi bi-person-circle me-1"></i>
{user.firstName}
</span>
</a>
<ul
className="dropdown-menu"
@@ -144,6 +202,11 @@ const Navbar: React.FC = () => {
<li>
<Link className="dropdown-item" to="/my-listings">
<i className="bi bi-list-ul me-2"></i>Owning
{pendingRequestsCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2">
{pendingRequestsCount}
</span>
)}
</Link>
</li>
<li>

View File

@@ -164,6 +164,9 @@ const MyListings: React.FC = () => {
fetchOwnerRentals();
fetchAvailableChecks(); // Refresh available checks after rental confirmation
// Notify Navbar to update pending count
window.dispatchEvent(new CustomEvent("rentalStatusChanged"));
} catch (err: any) {
console.error("Failed to accept rental request:", err);

View File

@@ -205,6 +205,7 @@ export const rentalAPI = {
createRental: (data: any) => api.post("/rentals", data),
getMyRentals: () => api.get("/rentals/my-rentals"),
getMyListings: () => api.get("/rentals/my-listings"),
getPendingRequestsCount: () => api.get("/rentals/pending-requests-count"),
updateRentalStatus: (id: string, status: string) =>
api.put(`/rentals/${id}/status`, { status }),
markAsCompleted: (id: string) => api.post(`/rentals/${id}/mark-completed`),
@@ -292,4 +293,9 @@ export const conditionCheckAPI = {
getAvailableChecks: () => api.get("/condition-checks"),
};
export const feedbackAPI = {
submitFeedback: (data: { feedbackText: string; url?: string }) =>
api.post("/feedback", data),
};
export default api;

View File

@@ -293,3 +293,13 @@ export interface RefundPreview {
reason: string;
totalAmount: number;
}
export interface Feedback {
id: string;
userId: string;
feedbackText: string;
userAgent?: string;
url?: string;
createdAt: string;
updatedAt: string;
}