reviews and review history

This commit is contained in:
jackiettran
2025-08-25 16:12:30 -04:00
parent 5d85f77a19
commit 601e11b7e8
7 changed files with 864 additions and 274 deletions

View File

@@ -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<ReviewDetailsModalProps> = ({
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 (
<div
className="modal d-block"
tabIndex={-1}
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Review Details</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<div className="modal-body">
{/* Header with user info */}
{rental.item && (
<div className="mb-4 text-center">
<h6 className="mb-1">{rental.item.name}</h6>
<small className="text-muted">
{formatDateTime(rental.startDate, rental.startTime)} to{" "}
{formatDateTime(rental.endDate, rental.endTime)}
</small>
</div>
)}
{/* What I Sent Section */}
{((isRenter &&
(rental.itemPrivateMessage ||
rental.itemReview ||
rental.itemRating)) ||
(!isRenter &&
(rental.renterPrivateMessage ||
rental.renterReview ||
rental.renterRating))) && (
<div className="mb-4">
<h6 className="text-primary mb-3">
<i className="bi bi-arrow-up-right-circle me-2"></i>
What I Sent
</h6>
{/* My Private Message */}
{((isRenter && rental.itemPrivateMessage) ||
(!isRenter && rental.renterPrivateMessage)) && (
<div className="mb-3">
<small className="text-muted fw-bold">
Private Note to {isRenter ? "Owner" : "Renter"}:
</small>
<div className="border rounded p-2 mt-1">
{isRenter
? rental.itemPrivateMessage
: rental.renterPrivateMessage}
</div>
</div>
)}
{/* My Public Review */}
{((isRenter && (rental.itemReview || rental.itemRating)) ||
(!isRenter &&
(rental.renterReview || rental.renterRating))) && (
<div className="mb-3">
<small className="text-muted fw-bold">
Public Review of {isRenter ? "Item" : "Renter"}:
</small>
<div className="border rounded p-2 mt-1">
{((isRenter && rental.itemRating) ||
(!isRenter && rental.renterRating)) && (
<div className="d-flex align-items-center mb-2">
<StarRating
rating={
isRenter
? rental.itemRating!
: rental.renterRating!
}
size="small"
/>
</div>
)}
{((isRenter && rental.itemReview) ||
(!isRenter && rental.renterReview)) && (
<p className="small mb-0">
{isRenter ? rental.itemReview : rental.renterReview}
</p>
)}
</div>
</div>
)}
</div>
)}
{/* What I Received Section */}
{((isRenter &&
(rental.renterPrivateMessage ||
rental.renterReview ||
rental.renterRating)) ||
(!isRenter &&
(rental.itemPrivateMessage ||
rental.itemReview ||
rental.itemRating))) && (
<div className="mb-4">
<h6 className="text-success mb-3">
<i className="bi bi-arrow-down-left-circle me-2"></i>
What I Received
</h6>
{/* Their Private Message */}
{((isRenter && rental.renterPrivateMessage) ||
(!isRenter && rental.itemPrivateMessage)) && (
<div className="mb-3">
<small className="text-muted fw-bold">
Private Note from {isRenter ? "Owner" : "Renter"}:
</small>
<div className="border rounded p-2 mt-1">
{isRenter
? rental.renterPrivateMessage
: rental.itemPrivateMessage}
</div>
</div>
)}
{/* Their Public Review */}
{((isRenter && (rental.renterReview || rental.renterRating)) ||
(!isRenter && (rental.itemReview || rental.itemRating))) && (
<div className="mb-3">
<small className="text-muted fw-bold">
{isRenter
? "Owner's Review of Me:"
: "Renter's Review of Item:"}
</small>
<div className="border rounded p-2 mt-1">
{((isRenter && rental.renterRating) ||
(!isRenter && rental.itemRating)) && (
<div className="d-flex align-items-center mb-2">
<StarRating
rating={
isRenter
? rental.renterRating!
: rental.itemRating!
}
size="small"
/>
</div>
)}
{((isRenter && rental.renterReview) ||
(!isRenter && rental.itemReview)) && (
<p className="small mb-0">
{isRenter ? rental.renterReview : rental.itemReview}
</p>
)}
</div>
</div>
)}
</div>
)}
{/* Empty state */}
{!rental.itemPrivateMessage &&
!rental.renterPrivateMessage &&
!rental.itemReview &&
!rental.renterReview &&
!rental.itemRating &&
!rental.renterRating && (
<div className="text-center text-muted py-4">
<i
className="bi bi-chat-text mb-3"
style={{ fontSize: "2rem" }}
></i>
<p>No review details available.</p>
</div>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
>
Close
</button>
</div>
</div>
</div>
</div>
);
};
export default ReviewDetailsModal;

View File

@@ -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<ReviewItemModalProps> = ({ show, onClose, rental, onSuccess }) => {
const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
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<string | null>(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<ReviewItemModalProps> = ({ 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<ReviewItemModalProps> = ({ show, onClose, rental
setRating(value);
};
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
onSuccess();
onClose();
};
if (!show) return null;
return (
<div className="modal d-block" tabIndex={-1} style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div
className="modal d-block"
tabIndex={-1}
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Review Item</h5>
<button type="button" className="btn-close" onClick={onClose}></button>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{rental.item && (
{rental.owner && rental.item && (
<div className="mb-4 text-center">
<h6>{rental.item.name}</h6>
<div className="d-flex justify-content-center mb-3">
{rental.owner.profileImage ? (
<img
src={rental.owner.profileImage}
alt={`${rental.owner.firstName} ${rental.owner.lastName}`}
className="rounded-circle"
style={{
width: "60px",
height: "60px",
objectFit: "cover",
}}
/>
) : (
<div
className="rounded-circle bg-primary d-flex align-items-center justify-content-center text-white fw-bold"
style={{ width: "60px", height: "60px" }}
>
{rental.owner.firstName[0]}
{rental.owner.lastName[0]}
</div>
)}
</div>
<h6 className="mb-1">
{rental.owner.firstName} {rental.owner.lastName}
</h6>
<p className="mb-1 text-muted small">{rental.item.name}</p>
<small className="text-muted">
Rented from {new Date(rental.startDate).toLocaleDateString()} to {new Date(rental.endDate).toLocaleDateString()}
{new Date(rental.startDate).toLocaleDateString()} to{" "}
{new Date(rental.endDate).toLocaleDateString()}
</small>
</div>
)}
@@ -78,55 +125,33 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({ show, onClose, rental
{error}
</div>
)}
<div className="mb-3">
<label className="form-label">Rating</label>
<div className="d-flex justify-content-center gap-1" style={{ fontSize: '2rem' }}>
<div
className="d-flex justify-content-center gap-1"
style={{ fontSize: "2rem" }}
>
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
className="btn btn-link p-0 text-decoration-none"
onClick={() => handleStarClick(star)}
style={{ color: star <= rating ? '#ffc107' : '#dee2e6' }}
style={{ color: star <= rating ? "#ffc107" : "#dee2e6" }}
>
<i className={`bi ${star <= rating ? 'bi-star-fill' : 'bi-star'}`}></i>
<i
className={`bi ${
star <= rating ? "bi-star-fill" : "bi-star"
}`}
></i>
</button>
))}
</div>
<div className="text-center mt-2">
<small className="text-muted">
{rating === 1 && 'Poor'}
{rating === 2 && 'Fair'}
{rating === 3 && 'Good'}
{rating === 4 && 'Very Good'}
{rating === 5 && 'Excellent'}
</small>
</div>
</div>
<div className="mb-3">
<label htmlFor="review" className="form-label">
Public Review <span className="text-danger">*</span>
</label>
<textarea
className="form-control"
id="review"
rows={4}
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder="Share your experience with this rental..."
required
disabled={submitting}
></textarea>
<small className="text-muted">
This will be visible to everyone. Tell others about the item condition, owner communication, and overall experience.
</small>
</div>
<div className="mb-3">
<label htmlFor="privateMessage" className="form-label">
Private Message to Owner <span className="text-muted">(Optional)</span>
Private Message to Owner{" "}
</label>
<textarea
className="form-control"
@@ -134,43 +159,66 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({ show, onClose, rental
rows={3}
value={privateMessage}
onChange={(e) => setPrivateMessage(e.target.value)}
placeholder="Send a private message to the owner (only they will see this)..."
placeholder=""
disabled={submitting}
></textarea>
</div>
<div className="mb-3">
<label htmlFor="review" className="form-label">
Public Review
</label>
<textarea
className="form-control"
id="review"
rows={4}
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder=""
disabled={submitting}
></textarea>
<small className="text-muted">
This message will only be visible to the owner. Use this for specific feedback or suggestions.
</small>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={submitting}
>
Cancel
</button>
<button
type="submit"
<button
type="submit"
className="btn btn-primary"
disabled={submitting || !review.trim()}
>
{submitting ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Submitting...
</>
) : (
'Submit Review'
"Submit Review"
)}
</button>
</div>
</form>
</div>
</div>
<SuccessModal
show={showSuccessModal}
onClose={handleSuccessModalClose}
title="Thank you for your review!"
message={successMessage}
/>
</div>
);
};
export default ReviewItemModal;
export default ReviewItemModal;

View File

@@ -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<ReviewRenterModalProps> = ({ show, onClose, rental, onSuccess }) => {
const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
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<string | null>(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<ReviewRenterModalProps> = ({ 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<ReviewRenterModalProps> = ({ show, onClose, re
setRating(value);
};
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
onSuccess();
onClose();
};
if (!show) return null;
return (
<div className="modal d-block" tabIndex={-1} style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div
className="modal d-block"
tabIndex={-1}
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Review Renter</h5>
<button type="button" className="btn-close" onClick={onClose}></button>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{rental.renter && (
{rental.renter && rental.item && (
<div className="mb-4 text-center">
<h6>{rental.renter.firstName} {rental.renter.lastName}</h6>
<div className="d-flex justify-content-center mb-3">
{rental.renter.profileImage ? (
<img
src={rental.renter.profileImage}
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
className="rounded-circle"
style={{
width: "60px",
height: "60px",
objectFit: "cover",
}}
/>
) : (
<div
className="rounded-circle bg-primary d-flex align-items-center justify-content-center text-white fw-bold"
style={{ width: "60px", height: "60px" }}
>
{rental.renter.firstName[0]}
{rental.renter.lastName[0]}
</div>
)}
</div>
<h6 className="mb-1">
{rental.renter.firstName} {rental.renter.lastName}
</h6>
<p className="mb-1 text-muted small">{rental.item.name}</p>
<small className="text-muted">
Rental period: {new Date(rental.startDate).toLocaleDateString()} to {new Date(rental.endDate).toLocaleDateString()}
{new Date(rental.startDate).toLocaleDateString()} to{" "}
{new Date(rental.endDate).toLocaleDateString()}
</small>
</div>
)}
@@ -78,55 +125,33 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({ show, onClose, re
{error}
</div>
)}
<div className="mb-3">
<label className="form-label">Rating</label>
<div className="d-flex justify-content-center gap-1" style={{ fontSize: '2rem' }}>
<div
className="d-flex justify-content-center gap-1"
style={{ fontSize: "2rem" }}
>
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
className="btn btn-link p-0 text-decoration-none"
onClick={() => handleStarClick(star)}
style={{ color: star <= rating ? '#ffc107' : '#dee2e6' }}
style={{ color: star <= rating ? "#ffc107" : "#dee2e6" }}
>
<i className={`bi ${star <= rating ? 'bi-star-fill' : 'bi-star'}`}></i>
<i
className={`bi ${
star <= rating ? "bi-star-fill" : "bi-star"
}`}
></i>
</button>
))}
</div>
<div className="text-center mt-2">
<small className="text-muted">
{rating === 1 && 'Poor'}
{rating === 2 && 'Fair'}
{rating === 3 && 'Good'}
{rating === 4 && 'Very Good'}
{rating === 5 && 'Excellent'}
</small>
</div>
</div>
<div className="mb-3">
<label htmlFor="review" className="form-label">
Public Review <span className="text-danger">*</span>
</label>
<textarea
className="form-control"
id="review"
rows={4}
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder="Share your experience with this renter..."
required
disabled={submitting}
></textarea>
<small className="text-muted">
This will be visible to everyone. Consider communication, condition of returned item, timeliness, and overall experience.
</small>
</div>
<div className="mb-3">
<label htmlFor="privateMessage" className="form-label">
Private Message to Renter <span className="text-muted">(Optional)</span>
Private Message to Renter{" "}
</label>
<textarea
className="form-control"
@@ -134,43 +159,66 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({ show, onClose, re
rows={3}
value={privateMessage}
onChange={(e) => setPrivateMessage(e.target.value)}
placeholder="Send a private message to the renter (only they will see this)..."
placeholder=""
disabled={submitting}
></textarea>
</div>
<div className="mb-3">
<label htmlFor="review" className="form-label">
Public Review
</label>
<textarea
className="form-control"
id="review"
rows={4}
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder=""
disabled={submitting}
></textarea>
<small className="text-muted">
This message will only be visible to the renter. Use this for specific feedback or suggestions.
</small>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={submitting}
>
Cancel
</button>
<button
type="submit"
<button
type="submit"
className="btn btn-primary"
disabled={submitting || !review.trim()}
>
{submitting ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Submitting...
</>
) : (
'Submit Review'
"Submit Review"
)}
</button>
</div>
</form>
</div>
</div>
<SuccessModal
show={showSuccessModal}
onClose={handleSuccessModalClose}
title="Thank you for your review!"
message={successMessage}
/>
</div>
);
};
export default ReviewRenterModal;
export default ReviewRenterModal;

View File

@@ -0,0 +1,75 @@
import React from 'react';
interface StarRatingProps {
rating: number;
size?: 'small' | 'medium' | 'large';
className?: string;
}
const StarRating: React.FC<StarRatingProps> = ({
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(
<i
key={`filled-${i}`}
className="bi bi-star-fill"
style={{ color: '#ffc107' }}
></i>
);
}
// Render half star if needed
if (hasHalfStar) {
stars.push(
<i
key="half"
className="bi bi-star-half"
style={{ color: '#ffc107' }}
></i>
);
}
// Render empty stars
const emptyStars = 5 - Math.ceil(rating);
for (let i = 0; i < emptyStars; i++) {
stars.push(
<i
key={`empty-${i}`}
className="bi bi-star"
style={{ color: '#dee2e6' }}
></i>
);
}
return stars;
};
return (
<div className={`d-inline-flex align-items-center gap-1 ${className}`} style={getSizeStyle()}>
{renderStars()}
</div>
);
};
export default StarRating;

View File

@@ -0,0 +1,60 @@
import React from 'react';
interface SuccessModalProps {
show: boolean;
onClose: () => void;
title?: string;
message: string;
}
const SuccessModal: React.FC<SuccessModalProps> = ({
show,
onClose,
title = "Success!",
message
}) => {
if (!show) return null;
return (
<div
className="modal d-block"
tabIndex={-1}
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header border-0 pb-0">
<div className="w-100 text-center">
<div
className="mx-auto mb-3 d-flex align-items-center justify-content-center bg-success rounded-circle"
style={{ width: '60px', height: '60px' }}
>
<i className="bi bi-check-lg text-white" style={{ fontSize: '2rem' }}></i>
</div>
<h5 className="modal-title">{title}</h5>
</div>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<div className="modal-body text-center pt-0">
<p className="mb-4">{message}</p>
</div>
<div className="modal-footer border-0 pt-0 justify-content-center">
<button
type="button"
className="btn btn-primary px-4"
onClick={onClose}
>
OK
</button>
</div>
</div>
</div>
</div>
);
};
export default SuccessModal;