layout and styling changes for RentItem
This commit is contained in:
@@ -25,7 +25,10 @@ const validateDates = (
|
|||||||
|
|
||||||
// Check if end datetime is after start datetime
|
// Check if end datetime is after start datetime
|
||||||
if (endDateTime <= startDateTime) {
|
if (endDateTime <= startDateTime) {
|
||||||
return { isValid: false, error: "End date/time must be after start date/time" };
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: "End date/time must be after start date/time",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isValid: true, error: null };
|
return { isValid: true, error: null };
|
||||||
@@ -65,7 +68,9 @@ const RentItem: React.FC = () => {
|
|||||||
const [costLoading, setCostLoading] = useState(false);
|
const [costLoading, setCostLoading] = useState(false);
|
||||||
const [costError, setCostError] = useState<string | null>(null);
|
const [costError, setCostError] = useState<string | null>(null);
|
||||||
const [completed, setCompleted] = useState(false);
|
const [completed, setCompleted] = useState(false);
|
||||||
const [dateValidationError, setDateValidationError] = useState<string | null>(null);
|
const [dateValidationError, setDateValidationError] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const formatDateTime = (date: Date | null) => {
|
const formatDateTime = (date: Date | null) => {
|
||||||
if (!date) return "";
|
if (!date) return "";
|
||||||
@@ -229,266 +234,272 @@ const RentItem: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-5">
|
<div style={{ backgroundColor: "#F7F9FB", minHeight: "100vh" }}>
|
||||||
<div className="row justify-content-center">
|
<div className="container pt-5">
|
||||||
<div className="col-md-10">
|
<div className="row justify-content-center">
|
||||||
<h1>Renting {item.name}</h1>
|
<div className="col-md-10">
|
||||||
|
<h1>Complete Your Request</h1>
|
||||||
|
|
||||||
{/* Email Verification Warning Banner */}
|
{/* Email Verification Warning Banner */}
|
||||||
{user && !user.isVerified && (
|
{user && !user.isVerified && (
|
||||||
<div className="alert alert-warning d-flex align-items-center mb-4">
|
<div className="alert alert-warning d-flex align-items-center mb-4">
|
||||||
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
<strong>Email verification required.</strong> Verify your email
|
<strong>Email verification required.</strong> Verify your
|
||||||
to book rentals.
|
email to book rentals.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-warning btn-sm ms-3"
|
||||||
|
onClick={() => setShowVerificationModal(true)}
|
||||||
|
>
|
||||||
|
Verify Now
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
)}
|
||||||
className="btn btn-warning btn-sm ms-3"
|
|
||||||
onClick={() => setShowVerificationModal(true)}
|
|
||||||
>
|
|
||||||
Verify Now
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{/* Pricing Card - appears first on mobile, right side on desktop */}
|
{/* Pricing Card - appears first on mobile, right side on desktop */}
|
||||||
<div className="col-md-4 order-first order-md-last mb-4 mb-md-0">
|
<div className="col-md-4 order-first order-md-last mb-4 mb-md-0">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{item.imageFilenames && item.imageFilenames[0] && (
|
{item.imageFilenames && item.imageFilenames[0] && (
|
||||||
<img
|
<img
|
||||||
src={getImageUrl(item.imageFilenames[0], 'medium')}
|
src={getImageUrl(item.imageFilenames[0], "medium")}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
className="img-fluid rounded mb-3"
|
className="img-fluid rounded mb-3"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
if (!target.dataset.fallback) {
|
if (!target.dataset.fallback) {
|
||||||
target.dataset.fallback = 'true';
|
target.dataset.fallback = "true";
|
||||||
target.src = getImageUrl(item.imageFilenames[0], 'original');
|
target.src = getImageUrl(
|
||||||
}
|
item.imageFilenames[0],
|
||||||
}}
|
"original"
|
||||||
style={{
|
);
|
||||||
width: "100%",
|
}
|
||||||
height: "150px",
|
}}
|
||||||
objectFit: "contain",
|
style={{
|
||||||
backgroundColor: "#f8f9fa",
|
width: "100%",
|
||||||
}}
|
height: "150px",
|
||||||
/>
|
objectFit: "contain",
|
||||||
)}
|
backgroundColor: "#f8f9fa",
|
||||||
|
}}
|
||||||
<h6>{item.name}</h6>
|
/>
|
||||||
<p className="text-muted small">
|
|
||||||
{item.city && item.state
|
|
||||||
? `${item.city}, ${item.state}`
|
|
||||||
: ""}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
{/* Pricing */}
|
|
||||||
<div className="mb-3 text-center">
|
|
||||||
{totalCost === 0 ? (
|
|
||||||
<h6>Free to Borrow</h6>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{item.pricePerHour && Number(item.pricePerHour) > 0 && (
|
|
||||||
<h6>${Math.floor(Number(item.pricePerHour))}/Hour</h6>
|
|
||||||
)}
|
|
||||||
{item.pricePerDay && Number(item.pricePerDay) > 0 && (
|
|
||||||
<h6>${Math.floor(Number(item.pricePerDay))}/Day</h6>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selected Dates */}
|
<h6>{item.name}</h6>
|
||||||
{rentalDates.startDateTime && rentalDates.endDateTime && (
|
<p className="text-muted small">
|
||||||
<div className="mb-3">
|
{item.city && item.state
|
||||||
<div className="small mb-1">
|
? `${item.city}, ${item.state}`
|
||||||
<strong>Check-in:</strong>{" "}
|
: ""}
|
||||||
{formatDateTime(rentalDates.startDateTime)}
|
</p>
|
||||||
</div>
|
|
||||||
<div className="small">
|
|
||||||
<strong>Check-out:</strong>{" "}
|
|
||||||
{formatDateTime(rentalDates.endDateTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Total Cost */}
|
|
||||||
<>
|
|
||||||
<hr />
|
<hr />
|
||||||
<div className="d-flex justify-content-between">
|
|
||||||
<strong>Total:</strong>
|
{/* Pricing */}
|
||||||
{costLoading ? (
|
<div className="mb-3 text-center">
|
||||||
<div
|
{totalCost === 0 ? (
|
||||||
className="spinner-border spinner-border-sm"
|
<h6>Free to Borrow</h6>
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="visually-hidden">
|
|
||||||
Calculating...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : costError ? (
|
|
||||||
<small className="text-danger">Error</small>
|
|
||||||
) : totalCost > 0 ? (
|
|
||||||
<strong>${totalCost}</strong>
|
|
||||||
) : (
|
) : (
|
||||||
<strong>$0</strong>
|
<>
|
||||||
|
{item.pricePerHour &&
|
||||||
|
Number(item.pricePerHour) > 0 && (
|
||||||
|
<h6>
|
||||||
|
${Math.floor(Number(item.pricePerHour))}/Hour
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
{item.pricePerDay && Number(item.pricePerDay) > 0 && (
|
||||||
|
<h6>${Math.floor(Number(item.pricePerDay))}/Day</h6>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* Selected Dates */}
|
||||||
|
{rentalDates.startDateTime && rentalDates.endDateTime && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="small mb-1">
|
||||||
|
<strong>Check-in:</strong>{" "}
|
||||||
|
{formatDateTime(rentalDates.startDateTime)}
|
||||||
|
</div>
|
||||||
|
<div className="small">
|
||||||
|
<strong>Check-out:</strong>{" "}
|
||||||
|
{formatDateTime(rentalDates.endDateTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Total Cost */}
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<strong>Total:</strong>
|
||||||
|
{costLoading ? (
|
||||||
|
<div
|
||||||
|
className="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span className="visually-hidden">
|
||||||
|
Calculating...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : costError ? (
|
||||||
|
<small className="text-danger">Error</small>
|
||||||
|
) : totalCost > 0 ? (
|
||||||
|
<strong>${totalCost}</strong>
|
||||||
|
) : (
|
||||||
|
<strong>$0</strong>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form - appears second on mobile, left side on desktop */}
|
{/* Form - appears second on mobile, left side on desktop */}
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
{completed ? (
|
{completed ? (
|
||||||
<div className="card mb-4">
|
<div className="mb-4">
|
||||||
<div className="card-body text-center">
|
<div className="text-center">
|
||||||
<div className="alert alert-success">
|
<div className="alert alert-success">
|
||||||
<i className="bi bi-check-circle-fill display-1 text-success mb-3"></i>
|
<i className="bi bi-check-circle-fill display-1 text-success mb-3"></i>
|
||||||
<h3>Rental Request Sent!</h3>
|
<h3>Rental Request Sent!</h3>
|
||||||
<p className="mb-3">
|
<p className="mb-3">
|
||||||
Your rental request has been submitted to the owner.
|
Your rental request has been submitted to the owner.
|
||||||
You'll only be charged if they approve your request.
|
You'll only be charged if they approve your request.
|
||||||
</p>
|
</p>
|
||||||
<div className="d-grid gap-2 d-md-block">
|
<div className="d-grid gap-2 d-md-block">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary me-2"
|
className="btn btn-primary me-2"
|
||||||
onClick={() => navigate("/renting")}
|
onClick={() => navigate("/renting")}
|
||||||
>
|
>
|
||||||
View My Rentals
|
View My Rentals
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-secondary"
|
className="btn btn-outline-secondary"
|
||||||
onClick={() => navigate("/")}
|
onClick={() => navigate("/")}
|
||||||
>
|
>
|
||||||
Continue Browsing
|
Continue Browsing
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="mb-4">
|
||||||
<div className="card mb-4">
|
<div>
|
||||||
<div className="card-body">
|
<div className="card mb-3">
|
||||||
<h5 className="card-title">
|
<div className="card-body">
|
||||||
{totalCost === 0
|
<h6 className="mb-3">
|
||||||
? "Complete Your Borrow Request"
|
What will you use this for?{" "}
|
||||||
: "Complete Your Rental Request"}
|
<span className="text-muted">(Optional)</span>
|
||||||
</h5>
|
</h6>
|
||||||
{totalCost > 0 && (
|
<textarea
|
||||||
<p className="text-muted small mb-3">
|
id="intendedUse"
|
||||||
Add your payment method to complete your rental request.
|
name="intendedUse"
|
||||||
You'll only be charged if the owner approves your
|
className="form-control"
|
||||||
request.
|
rows={3}
|
||||||
</p>
|
value={formData.intendedUse}
|
||||||
)}
|
onChange={handleChange}
|
||||||
|
placeholder="Let the owner know how you plan to use their item..."
|
||||||
<div className="mb-3">
|
maxLength={500}
|
||||||
<label htmlFor="intendedUse" className="form-label">
|
/>
|
||||||
What will you use this for?{" "}
|
<div className="form-text">
|
||||||
<span className="text-muted">(Optional)</span>
|
{formData.intendedUse.length}/500 characters
|
||||||
</label>
|
</div>
|
||||||
<textarea
|
</div>
|
||||||
id="intendedUse"
|
|
||||||
name="intendedUse"
|
|
||||||
className="form-control"
|
|
||||||
rows={3}
|
|
||||||
value={formData.intendedUse}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Let the owner know how you plan to use their item..."
|
|
||||||
maxLength={500}
|
|
||||||
/>
|
|
||||||
<div className="form-text">
|
|
||||||
{formData.intendedUse.length}/500 characters
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{dateValidationError ? (
|
||||||
|
<div className="alert alert-danger">
|
||||||
|
<i className="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
{dateValidationError}
|
||||||
|
</div>
|
||||||
|
) : !rentalDates.startDateTime ||
|
||||||
|
!rentalDates.endDateTime ||
|
||||||
|
!getRentalData() ? (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
<i className="bi bi-info-circle me-2"></i>
|
||||||
|
Please complete the rental dates and details above to
|
||||||
|
proceed with{" "}
|
||||||
|
{totalCost === 0
|
||||||
|
? "your borrow request"
|
||||||
|
: "payment setup"}
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
) : totalCost === 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="d-grid gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleFreeBorrow}
|
||||||
|
>
|
||||||
|
Confirm Borrow Request
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={() => navigate(`/items/${id}`)}
|
||||||
|
>
|
||||||
|
Cancel Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="card mb-3">
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="mb-3">
|
||||||
|
Add your payment method to complete your
|
||||||
|
request. You'll only be charged if the owner
|
||||||
|
approves your request
|
||||||
|
</h6>
|
||||||
|
<EmbeddedStripeCheckout
|
||||||
|
rentalData={getRentalData()}
|
||||||
|
onSuccess={() => setCompleted(true)}
|
||||||
|
onError={(error) => setError(error)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={() => navigate(`/items/${id}`)}
|
||||||
|
>
|
||||||
|
Cancel Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dateValidationError ? (
|
|
||||||
<div className="alert alert-danger">
|
|
||||||
<i className="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
{dateValidationError}
|
|
||||||
</div>
|
|
||||||
) : !rentalDates.startDateTime ||
|
|
||||||
!rentalDates.endDateTime ||
|
|
||||||
!getRentalData() ? (
|
|
||||||
<div className="alert alert-info">
|
|
||||||
<i className="bi bi-info-circle me-2"></i>
|
|
||||||
Please complete the rental dates and details above to
|
|
||||||
proceed with{" "}
|
|
||||||
{totalCost === 0
|
|
||||||
? "your borrow request"
|
|
||||||
: "payment setup"}
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
) : totalCost === 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="d-grid gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleFreeBorrow}
|
|
||||||
>
|
|
||||||
Confirm Borrow Request
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={() => navigate(`/items/${id}`)}
|
|
||||||
>
|
|
||||||
Cancel Request
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<EmbeddedStripeCheckout
|
|
||||||
rentalData={getRentalData()}
|
|
||||||
onSuccess={() => setCompleted(true)}
|
|
||||||
onError={(error) => setError(error)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="text-center mt-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={() => navigate(`/items/${id}`)}
|
|
||||||
>
|
|
||||||
Cancel Request
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email Verification Modal */}
|
{/* Email Verification Modal */}
|
||||||
{user && (
|
{user && (
|
||||||
<VerificationCodeModal
|
<VerificationCodeModal
|
||||||
show={showVerificationModal}
|
show={showVerificationModal}
|
||||||
onHide={() => {
|
onHide={() => {
|
||||||
setShowVerificationModal(false);
|
setShowVerificationModal(false);
|
||||||
setPendingSubmit(false);
|
setPendingSubmit(false);
|
||||||
}}
|
}}
|
||||||
email={user.email || ""}
|
email={user.email || ""}
|
||||||
onVerified={handleVerificationSuccess}
|
onVerified={handleVerificationSuccess}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user