Files
rentall-app/frontend/src/pages/RentItem.tsx
2025-11-01 22:33:59 -04:00

389 lines
13 KiB
TypeScript

import React, { useState, useEffect, useRef } from "react";
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import { Item } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI } from "../services/api";
import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout";
const RentItem: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const [searchParams] = useSearchParams();
const [item, setItem] = useState<Item | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState({
deliveryMethod: "pickup" as "pickup" | "delivery",
deliveryAddress: "",
});
const [manualSelection, setManualSelection] = useState({
startDate: searchParams.get("startDate") || "",
startTime: searchParams.get("startTime") || "09:00",
endDate: searchParams.get("endDate") || "",
endTime: searchParams.get("endTime") || "17:00",
});
const [totalCost, setTotalCost] = useState(0);
const [completed, setCompleted] = useState(false);
const convertToUTC = (dateString: string, timeString: string): string => {
if (!dateString || !timeString) {
throw new Error("Date and time are required");
}
// Create date in user's local timezone
const localDateTime = new Date(`${dateString}T${timeString}`);
// Return UTC ISO string
return localDateTime.toISOString();
};
const formatDate = (dateString: string) => {
if (!dateString) return "";
// Use safe date parsing to avoid timezone issues
const [year, month, day] = dateString.split("-");
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return date.toLocaleDateString();
};
const formatTime = (timeString: string) => {
if (!timeString) return "";
const [hour, minute] = timeString.split(":");
const hour12 =
parseInt(hour) === 0
? 12
: parseInt(hour) > 12
? parseInt(hour) - 12
: parseInt(hour);
const period = parseInt(hour) < 12 ? "AM" : "PM";
return `${hour12}:${minute} ${period}`;
};
const calculateTotalCost = () => {
if (!item || !manualSelection.startDate || !manualSelection.endDate) {
setTotalCost(0);
return;
}
const startDateTime = new Date(
`${manualSelection.startDate}T${manualSelection.startTime}`
);
const endDateTime = new Date(
`${manualSelection.endDate}T${manualSelection.endTime}`
);
const diffMs = endDateTime.getTime() - startDateTime.getTime();
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
let cost = 0;
if (item.pricePerHour && diffHours <= 24) {
cost = diffHours * Number(item.pricePerHour);
} else if (item.pricePerDay) {
cost = diffDays * Number(item.pricePerDay);
}
setTotalCost(cost);
};
useEffect(() => {
fetchItem();
}, [id]);
useEffect(() => {
calculateTotalCost();
}, [item, manualSelection]);
const fetchItem = async () => {
try {
const response = await itemAPI.getItem(id!);
setItem(response.data);
// Check if item is available
if (!response.data.availability) {
setError("This item is not available for rent");
}
// Check if user is trying to rent their own item
if (response.data.ownerId === user?.id) {
setError("You cannot rent your own item");
}
} catch (err: any) {
setError(err.response?.data?.message || "Failed to fetch item");
} finally {
setLoading(false);
}
};
const getRentalData = () => {
try {
const startDateTime = convertToUTC(
manualSelection.startDate,
manualSelection.startTime
);
const endDateTime = convertToUTC(
manualSelection.endDate,
manualSelection.endTime
);
return {
itemId: id,
startDateTime,
endDateTime,
deliveryMethod: formData.deliveryMethod,
deliveryAddress: formData.deliveryAddress,
totalAmount: totalCost,
};
} catch (error: any) {
setError(error.message);
return null;
}
};
const handleFreeBorrow = async () => {
const rentalData = getRentalData();
if (!rentalData) return;
try {
setError(null);
await rentalAPI.createRental(rentalData);
setCompleted(true);
} catch (error: any) {
setError(
error.response?.data?.error || "Failed to create rental request"
);
}
};
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
if (loading) {
return (
<div className="container mt-5">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
if (
!item ||
error === "You cannot rent your own item" ||
error === "This item is not available for rent"
) {
return (
<div className="container mt-5">
<div className="alert alert-danger" role="alert">
{error || "Item not found"}
</div>
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
Go Back
</button>
</div>
);
}
return (
<div className="container mt-4">
<div className="row">
<div className="col-md-8">
<h1>Renting {item.name}</h1>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="row">
<div className="col-md-8">
{completed ? (
<div className="card mb-4">
<div className="card-body text-center">
<div className="alert alert-success">
<i className="bi bi-check-circle-fill display-1 text-success mb-3"></i>
<h3>Rental Request Sent!</h3>
<p className="mb-3">
Your rental request has been submitted to the owner.
You'll only be charged if they approve your request.
</p>
<div className="d-grid gap-2 d-md-block">
<button
className="btn btn-primary me-2"
onClick={() => navigate("/renting")}
>
View My Rentals
</button>
<button
className="btn btn-outline-secondary"
onClick={() => navigate("/")}
>
Continue Browsing
</button>
</div>
</div>
</div>
</div>
) : (
<div className="card mb-4">
<div className="card-body">
<h5 className="card-title">
{totalCost === 0
? "Complete Your Borrow Request"
: "Complete Your Rental Request"}
</h5>
{totalCost > 0 && (
<p className="text-muted small mb-3">
Add your payment method to complete your rental request.
You'll only be charged if the owner approves your
request.
</p>
)}
{!manualSelection.startDate ||
!manualSelection.endDate ||
!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 className="col-md-4">
<div className="card">
<div className="card-body">
{item.images && item.images[0] && (
<img
src={item.images[0]}
alt={item.name}
className="img-fluid rounded mb-3"
style={{
width: "100%",
height: "150px",
objectFit: "cover",
}}
/>
)}
<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 */}
{manualSelection.startDate && manualSelection.endDate && (
<div className="mb-3">
<div className="small mb-1">
<strong>Check-in:</strong>{" "}
{formatDate(manualSelection.startDate)} at{" "}
{formatTime(manualSelection.startTime)}
</div>
<div className="small">
<strong>Check-out:</strong>{" "}
{formatDate(manualSelection.endDate)} at{" "}
{formatTime(manualSelection.endTime)}
</div>
</div>
)}
{/* Total Cost */}
{totalCost > 0 && (
<>
<hr />
<div className="d-flex justify-content-between">
<strong>Total:</strong>
<strong>${totalCost}</strong>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default RentItem;