273 lines
8.4 KiB
TypeScript
273 lines
8.4 KiB
TypeScript
import React, { useState, useEffect } 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 StripePaymentForm from "../components/StripePaymentForm";
|
|
|
|
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 formatDate = (dateString: string) => {
|
|
if (!dateString) return "";
|
|
return new Date(dateString).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 handlePaymentSuccess = () => {
|
|
console.log("Stripe checkout session created successfully");
|
|
};
|
|
|
|
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">
|
|
<div className="card mb-4">
|
|
<div className="card-body">
|
|
<h5 className="card-title">Payment</h5>
|
|
|
|
<StripePaymentForm
|
|
total={totalCost}
|
|
itemName={item.name}
|
|
rentalData={{
|
|
itemId: item.id,
|
|
startDate: manualSelection.startDate,
|
|
endDate: manualSelection.endDate,
|
|
startTime: manualSelection.startTime,
|
|
endTime: manualSelection.endTime,
|
|
totalAmount: totalCost,
|
|
deliveryMethod: "pickup",
|
|
}}
|
|
onSuccess={handlePaymentSuccess}
|
|
onError={(error) => setError(error)}
|
|
disabled={
|
|
!manualSelection.startDate || !manualSelection.endDate
|
|
}
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary mt-2"
|
|
onClick={() => navigate(`/items/${id}`)}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</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}`
|
|
: item.location}
|
|
</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;
|