From 601e11b7e80052570821d76646963dedfc6a32dd Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:12:30 -0400 Subject: [PATCH] reviews and review history --- .../src/components/ReviewDetailsModal.tsx | 229 +++++++++++++++ frontend/src/components/ReviewModal.tsx | 182 +++++++----- frontend/src/components/ReviewRenterModal.tsx | 182 +++++++----- frontend/src/components/StarRating.tsx | 75 +++++ frontend/src/components/SuccessModal.tsx | 60 ++++ frontend/src/pages/MyListings.tsx | 132 +++++---- frontend/src/pages/Profile.tsx | 278 ++++++++++++------ 7 files changed, 864 insertions(+), 274 deletions(-) create mode 100644 frontend/src/components/ReviewDetailsModal.tsx create mode 100644 frontend/src/components/StarRating.tsx create mode 100644 frontend/src/components/SuccessModal.tsx diff --git a/frontend/src/components/ReviewDetailsModal.tsx b/frontend/src/components/ReviewDetailsModal.tsx new file mode 100644 index 0000000..e7758ee --- /dev/null +++ b/frontend/src/components/ReviewDetailsModal.tsx @@ -0,0 +1,229 @@ +import React from "react"; +import { Rental } from "../types"; +import StarRating from "./StarRating"; + +interface ReviewDetailsModalProps { + show: boolean; + onClose: () => void; + rental: Rental; + userType: "renter" | "owner"; +} + +const ReviewDetailsModal: React.FC = ({ + show, + onClose, + rental, + userType, +}) => { + if (!show) return null; + + const formatDateTime = (dateString: string, timeString?: string) => { + const date = new Date(dateString).toLocaleDateString(); + const formattedTime = timeString + ? (() => { + try { + const [hour, minute] = timeString.split(":"); + const hourNum = parseInt(hour); + const hour12 = + hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum; + const period = hourNum < 12 ? "AM" : "PM"; + return `${hour12}:${minute} ${period}`; + } catch { + return ""; + } + })() + : ""; + return formattedTime ? `${date} at ${formattedTime}` : date; + }; + + const isRenter = userType === "renter"; + + return ( +
+
+
+
+
Review Details
+ +
+
+ {/* Header with user info */} + {rental.item && ( +
+
{rental.item.name}
+ + {formatDateTime(rental.startDate, rental.startTime)} to{" "} + {formatDateTime(rental.endDate, rental.endTime)} + +
+ )} + + {/* What I Sent Section */} + {((isRenter && + (rental.itemPrivateMessage || + rental.itemReview || + rental.itemRating)) || + (!isRenter && + (rental.renterPrivateMessage || + rental.renterReview || + rental.renterRating))) && ( +
+
+ + What I Sent +
+ + {/* My Private Message */} + {((isRenter && rental.itemPrivateMessage) || + (!isRenter && rental.renterPrivateMessage)) && ( +
+ + Private Note to {isRenter ? "Owner" : "Renter"}: + +
+ {isRenter + ? rental.itemPrivateMessage + : rental.renterPrivateMessage} +
+
+ )} + + {/* My Public Review */} + {((isRenter && (rental.itemReview || rental.itemRating)) || + (!isRenter && + (rental.renterReview || rental.renterRating))) && ( +
+ + Public Review of {isRenter ? "Item" : "Renter"}: + +
+ {((isRenter && rental.itemRating) || + (!isRenter && rental.renterRating)) && ( +
+ +
+ )} + {((isRenter && rental.itemReview) || + (!isRenter && rental.renterReview)) && ( +

+ {isRenter ? rental.itemReview : rental.renterReview} +

+ )} +
+
+ )} +
+ )} + + {/* What I Received Section */} + {((isRenter && + (rental.renterPrivateMessage || + rental.renterReview || + rental.renterRating)) || + (!isRenter && + (rental.itemPrivateMessage || + rental.itemReview || + rental.itemRating))) && ( +
+
+ + What I Received +
+ + {/* Their Private Message */} + {((isRenter && rental.renterPrivateMessage) || + (!isRenter && rental.itemPrivateMessage)) && ( +
+ + Private Note from {isRenter ? "Owner" : "Renter"}: + +
+ {isRenter + ? rental.renterPrivateMessage + : rental.itemPrivateMessage} +
+
+ )} + + {/* Their Public Review */} + {((isRenter && (rental.renterReview || rental.renterRating)) || + (!isRenter && (rental.itemReview || rental.itemRating))) && ( +
+ + {isRenter + ? "Owner's Review of Me:" + : "Renter's Review of Item:"} + +
+ {((isRenter && rental.renterRating) || + (!isRenter && rental.itemRating)) && ( +
+ +
+ )} + {((isRenter && rental.renterReview) || + (!isRenter && rental.itemReview)) && ( +

+ {isRenter ? rental.renterReview : rental.itemReview} +

+ )} +
+
+ )} +
+ )} + + {/* Empty state */} + {!rental.itemPrivateMessage && + !rental.renterPrivateMessage && + !rental.itemReview && + !rental.renterReview && + !rental.itemRating && + !rental.renterRating && ( +
+ +

No review details available.

+
+ )} +
+
+ +
+
+
+
+ ); +}; + +export default ReviewDetailsModal; diff --git a/frontend/src/components/ReviewModal.tsx b/frontend/src/components/ReviewModal.tsx index dec0c46..b4efb42 100644 --- a/frontend/src/components/ReviewModal.tsx +++ b/frontend/src/components/ReviewModal.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; -import { rentalAPI } from '../services/api'; -import { Rental } from '../types'; +import React, { useState } from "react"; +import { rentalAPI } from "../services/api"; +import { Rental } from "../types"; +import SuccessModal from "./SuccessModal"; interface ReviewItemModalProps { show: boolean; @@ -9,12 +10,19 @@ interface ReviewItemModalProps { onSuccess: () => void; } -const ReviewItemModal: React.FC = ({ show, onClose, rental, onSuccess }) => { +const ReviewItemModal: React.FC = ({ + show, + onClose, + rental, + onSuccess, +}) => { const [rating, setRating] = useState(5); - const [review, setReview] = useState(''); - const [privateMessage, setPrivateMessage] = useState(''); + const [review, setReview] = useState(""); + const [privateMessage, setPrivateMessage] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -25,24 +33,23 @@ const ReviewItemModal: React.FC = ({ show, onClose, rental const response = await rentalAPI.reviewItem(rental.id, { rating, review, - privateMessage + privateMessage, }); // Reset form setRating(5); - setReview(''); - setPrivateMessage(''); - onSuccess(); - onClose(); - - // Show success message based on review visibility + setReview(""); + setPrivateMessage(""); + + // Show success modal with appropriate message if (response.data.reviewVisible) { - alert('Review published successfully!'); + setSuccessMessage("Review published successfully!"); } else { - alert('Review submitted! It will be published when both parties have reviewed or after 10 minutes.'); + setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes."); } + setShowSuccessModal(true); } catch (err: any) { - setError(err.response?.data?.error || 'Failed to submit review'); + setError(err.response?.data?.error || "Failed to submit review"); } finally { setSubmitting(false); } @@ -52,23 +59,63 @@ const ReviewItemModal: React.FC = ({ show, onClose, rental setRating(value); }; + const handleSuccessModalClose = () => { + setShowSuccessModal(false); + onSuccess(); + onClose(); + }; + if (!show) return null; return ( -
+
Review Item
- +
- {rental.item && ( + {rental.owner && rental.item && (
-
{rental.item.name}
+
+ {rental.owner.profileImage ? ( + {`${rental.owner.firstName} + ) : ( +
+ {rental.owner.firstName[0]} + {rental.owner.lastName[0]} +
+ )} +
+
+ {rental.owner.firstName} {rental.owner.lastName} +
+

{rental.item.name}

- Rented from {new Date(rental.startDate).toLocaleDateString()} to {new Date(rental.endDate).toLocaleDateString()} + {new Date(rental.startDate).toLocaleDateString()} to{" "} + {new Date(rental.endDate).toLocaleDateString()}
)} @@ -78,55 +125,33 @@ const ReviewItemModal: React.FC = ({ show, onClose, rental {error}
)} - +
- -
+
{[1, 2, 3, 4, 5].map((star) => ( ))}
-
- - {rating === 1 && 'Poor'} - {rating === 2 && 'Fair'} - {rating === 3 && 'Good'} - {rating === 4 && 'Very Good'} - {rating === 5 && 'Excellent'} - -
-
- -
- - - - This will be visible to everyone. Tell others about the item condition, owner communication, and overall experience. -
+
+ +
+ + - - This message will only be visible to the owner. Use this for specific feedback or suggestions. -
- -
+ +
); }; -export default ReviewItemModal; \ No newline at end of file +export default ReviewItemModal; diff --git a/frontend/src/components/ReviewRenterModal.tsx b/frontend/src/components/ReviewRenterModal.tsx index b613ca8..0de6023 100644 --- a/frontend/src/components/ReviewRenterModal.tsx +++ b/frontend/src/components/ReviewRenterModal.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; -import { rentalAPI } from '../services/api'; -import { Rental } from '../types'; +import React, { useState } from "react"; +import { rentalAPI } from "../services/api"; +import { Rental } from "../types"; +import SuccessModal from "./SuccessModal"; interface ReviewRenterModalProps { show: boolean; @@ -9,12 +10,19 @@ interface ReviewRenterModalProps { onSuccess: () => void; } -const ReviewRenterModal: React.FC = ({ show, onClose, rental, onSuccess }) => { +const ReviewRenterModal: React.FC = ({ + show, + onClose, + rental, + onSuccess, +}) => { const [rating, setRating] = useState(5); - const [review, setReview] = useState(''); - const [privateMessage, setPrivateMessage] = useState(''); + const [review, setReview] = useState(""); + const [privateMessage, setPrivateMessage] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -25,24 +33,23 @@ const ReviewRenterModal: React.FC = ({ show, onClose, re const response = await rentalAPI.reviewRenter(rental.id, { rating, review, - privateMessage + privateMessage, }); // Reset form setRating(5); - setReview(''); - setPrivateMessage(''); - onSuccess(); - onClose(); - - // Show success message based on review visibility + setReview(""); + setPrivateMessage(""); + + // Show success modal with appropriate message if (response.data.reviewVisible) { - alert('Review published successfully!'); + setSuccessMessage("Review published successfully!"); } else { - alert('Review submitted! It will be published when both parties have reviewed or after 10 minutes.'); + setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes."); } + setShowSuccessModal(true); } catch (err: any) { - setError(err.response?.data?.error || 'Failed to submit review'); + setError(err.response?.data?.error || "Failed to submit review"); } finally { setSubmitting(false); } @@ -52,23 +59,63 @@ const ReviewRenterModal: React.FC = ({ show, onClose, re setRating(value); }; + const handleSuccessModalClose = () => { + setShowSuccessModal(false); + onSuccess(); + onClose(); + }; + if (!show) return null; return ( -
+
Review Renter
- +
- {rental.renter && ( + {rental.renter && rental.item && (
-
{rental.renter.firstName} {rental.renter.lastName}
+
+ {rental.renter.profileImage ? ( + {`${rental.renter.firstName} + ) : ( +
+ {rental.renter.firstName[0]} + {rental.renter.lastName[0]} +
+ )} +
+
+ {rental.renter.firstName} {rental.renter.lastName} +
+

{rental.item.name}

- Rental period: {new Date(rental.startDate).toLocaleDateString()} to {new Date(rental.endDate).toLocaleDateString()} + {new Date(rental.startDate).toLocaleDateString()} to{" "} + {new Date(rental.endDate).toLocaleDateString()}
)} @@ -78,55 +125,33 @@ const ReviewRenterModal: React.FC = ({ show, onClose, re {error}
)} - +
- -
+
{[1, 2, 3, 4, 5].map((star) => ( ))}
-
- - {rating === 1 && 'Poor'} - {rating === 2 && 'Fair'} - {rating === 3 && 'Good'} - {rating === 4 && 'Very Good'} - {rating === 5 && 'Excellent'} - -
-
- -
- - - - This will be visible to everyone. Consider communication, condition of returned item, timeliness, and overall experience. -
+
+ +
+ + - - This message will only be visible to the renter. Use this for specific feedback or suggestions. -
- -
+ +
); }; -export default ReviewRenterModal; \ No newline at end of file +export default ReviewRenterModal; diff --git a/frontend/src/components/StarRating.tsx b/frontend/src/components/StarRating.tsx new file mode 100644 index 0000000..61876f7 --- /dev/null +++ b/frontend/src/components/StarRating.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +interface StarRatingProps { + rating: number; + size?: 'small' | 'medium' | 'large'; + className?: string; +} + +const StarRating: React.FC = ({ + rating, + size = 'small', + className = '' +}) => { + const getSizeStyle = () => { + switch (size) { + case 'large': + return { fontSize: '1.5rem' }; + case 'medium': + return { fontSize: '1.2rem' }; + case 'small': + default: + return { fontSize: '1rem' }; + } + }; + + const renderStars = () => { + const stars = []; + const fullStars = Math.floor(rating); + const hasHalfStar = rating % 1 !== 0; + + // Render filled stars + for (let i = 0; i < fullStars; i++) { + stars.push( + + ); + } + + // Render half star if needed + if (hasHalfStar) { + stars.push( + + ); + } + + // Render empty stars + const emptyStars = 5 - Math.ceil(rating); + for (let i = 0; i < emptyStars; i++) { + stars.push( + + ); + } + + return stars; + }; + + return ( +
+ {renderStars()} +
+ ); +}; + +export default StarRating; \ No newline at end of file diff --git a/frontend/src/components/SuccessModal.tsx b/frontend/src/components/SuccessModal.tsx new file mode 100644 index 0000000..bae4508 --- /dev/null +++ b/frontend/src/components/SuccessModal.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +interface SuccessModalProps { + show: boolean; + onClose: () => void; + title?: string; + message: string; +} + +const SuccessModal: React.FC = ({ + show, + onClose, + title = "Success!", + message +}) => { + if (!show) return null; + + return ( +
+
+
+
+
+
+ +
+
{title}
+
+ +
+
+

{message}

+
+
+ +
+
+
+
+ ); +}; + +export default SuccessModal; \ No newline at end of file diff --git a/frontend/src/pages/MyListings.tsx b/frontend/src/pages/MyListings.tsx index 9ca5958..028b741 100644 --- a/frontend/src/pages/MyListings.tsx +++ b/frontend/src/pages/MyListings.tsx @@ -36,7 +36,8 @@ const MyListings: React.FC = () => { const [error, setError] = useState(""); // Owner rental management state const [showReviewRenterModal, setShowReviewRenterModal] = useState(false); - const [selectedRentalForReview, setSelectedRentalForReview] = useState(null); + const [selectedRentalForReview, setSelectedRentalForReview] = + useState(null); useEffect(() => { fetchMyListings(); @@ -127,16 +128,19 @@ const MyListings: React.FC = () => { const handleCompleteClick = async (rental: Rental) => { try { - console.log('Marking rental as completed:', rental.id); + console.log("Marking rental as completed:", rental.id); await rentalAPI.markAsCompleted(rental.id); - + setSelectedRentalForReview(rental); setShowReviewRenterModal(true); - + fetchOwnerRentals(); } catch (err: any) { - console.error('Error marking rental as completed:', err); - alert("Failed to mark rental as completed: " + (err.response?.data?.error || err.message)); + console.error("Error marking rental as completed:", err); + alert( + "Failed to mark rental as completed: " + + (err.response?.data?.error || err.message) + ); } }; @@ -145,12 +149,15 @@ const MyListings: React.FC = () => { }; // Filter owner rentals - const allOwnerRentals = ownerRentals.filter((r) => - ["pending", "confirmed", "active"].includes(r.status) - ).sort((a, b) => { - const statusOrder = { "pending": 0, "confirmed": 1, "active": 2 }; - return statusOrder[a.status as keyof typeof statusOrder] - statusOrder[b.status as keyof typeof statusOrder]; - }); + const allOwnerRentals = ownerRentals + .filter((r) => ["pending", "confirmed", "active"].includes(r.status)) + .sort((a, b) => { + const statusOrder = { pending: 0, confirmed: 1, active: 2 }; + return ( + statusOrder[a.status as keyof typeof statusOrder] - + statusOrder[b.status as keyof typeof statusOrder] + ); + }); if (loading) { return ( @@ -167,7 +174,7 @@ const MyListings: React.FC = () => { return (
-

My Listings

+

Owning

Add New Item @@ -184,7 +191,7 @@ const MyListings: React.FC = () => {

- Rental Requests ({allOwnerRentals.length}) + Rental Requests

{allOwnerRentals.map((rental) => ( @@ -205,24 +212,35 @@ const MyListings: React.FC = () => { {rental.renter && (

- Renter: {rental.renter.firstName} {rental.renter.lastName} + Renter: {rental.renter.firstName}{" "} + {rental.renter.lastName}

)}
- - {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + + {rental.status.charAt(0).toUpperCase() + + rental.status.slice(1)}

Period:
- {formatDateTime(rental.startDate, rental.startTime)} - {formatDateTime(rental.endDate, rental.endTime)} + {formatDateTime( + rental.startDate, + rental.startTime + )} - {formatDateTime(rental.endDate, rental.endTime)}

@@ -231,7 +249,10 @@ const MyListings: React.FC = () => { {rental.itemPrivateMessage && rental.itemReviewVisible && (

- Private Note from Renter: + + Private + Note from Renter: +
{rental.itemPrivateMessage}
@@ -254,7 +275,8 @@ const MyListings: React.FC = () => { )} - {(rental.status === "active" || rental.status === "confirmed") && ( + {(rental.status === "active" || + rental.status === "confirmed") && ( + )} + {rental.itemReviewSubmittedAt && + !rental.itemReviewVisible && ( +
+ + Review Submitted +
+ )} + {((rental.renterPrivateMessage && + rental.renterReviewVisible) || + (rental.itemReviewVisible && + rental.itemRating)) && ( )} - {rental.itemReviewSubmittedAt && !rental.itemReviewVisible && ( -
- - Review Submitted -
- )} - {rental.itemReviewVisible && rental.itemRating && ( -
- - Review Published ({rental.itemRating}/5) -
- )} - {rental.status === "completed" && rental.rating && !rental.itemRating && ( -
- - Reviewed ({rental.rating}/5) -
- )} + {rental.status === "completed" && + rental.rating && + !rental.itemRating && ( +
+ + Reviewed ({rental.rating}/5) +
+ )}
@@ -889,79 +947,102 @@ const Profile: React.FC = () => {
{pastOwnerRentals.map((rental) => ( -
+
{rental.item?.images && rental.item.images[0] && ( {rental.item.name} )}
- {rental.item ? rental.item.name : "Item Unavailable"} + {rental.item + ? rental.item.name + : "Item Unavailable"}
- + {rental.renter && (

- Renter: {rental.renter.firstName} {rental.renter.lastName} + Renter:{" "} + {rental.renter.firstName}{" "} + {rental.renter.lastName}

)} - +
- {rental.status.charAt(0).toUpperCase() + rental.status.slice(1)} + {rental.status.charAt(0).toUpperCase() + + rental.status.slice(1)}

Period:
- {formatDateTime(rental.startDate, rental.startTime)} - {formatDateTime(rental.endDate, rental.endTime)} + {formatDateTime( + rental.startDate, + rental.startTime + )}{" "} + -{" "} + {formatDateTime( + rental.endDate, + rental.endTime + )}

Total: ${rental.totalAmount}

- {rental.itemPrivateMessage && rental.itemReviewVisible && ( -
- Private Note from Renter: -
- {rental.itemPrivateMessage} -
- )} -
- {rental.status === "completed" && !rental.renterRating && !rental.renterReviewSubmittedAt && ( + {rental.status === "completed" && + !rental.renterRating && + !rental.renterReviewSubmittedAt && ( + + )} + {rental.renterReviewSubmittedAt && + !rental.renterReviewVisible && ( +
+ + Review Submitted +
+ )} + {((rental.itemPrivateMessage && + rental.itemReviewVisible) || + (rental.renterReviewVisible && + rental.renterRating)) && ( )} - {rental.renterReviewSubmittedAt && !rental.renterReviewVisible && ( -
- - Review Submitted -
- )} - {rental.renterReviewVisible && rental.renterRating && ( -
- - Review Published ({rental.renterRating}/5) -
- )}
@@ -972,13 +1053,20 @@ const Profile: React.FC = () => { )} {/* Empty State */} - {pastRenterRentals.length === 0 && pastOwnerRentals.length === 0 && ( -
- -
No Rental History
-

Your completed rentals and rental requests will appear here.

-
- )} + {pastRenterRentals.length === 0 && + pastOwnerRentals.length === 0 && ( +
+ +
No Rental History
+

+ Your completed rentals and rental requests will appear + here. +

+
+ )} )}
@@ -1354,6 +1442,16 @@ const Profile: React.FC = () => { onSuccess={handleReviewRenterSuccess} /> )} + + {/* Review Details Modal */} + {selectedRentalForDetails && ( + + )}
); };