239 lines
7.7 KiB
TypeScript
239 lines
7.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { Item, Rental } from '../types';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { itemAPI, rentalAPI } from '../services/api';
|
|
import LocationMap from '../components/LocationMap';
|
|
import ItemReviews from '../components/ItemReviews';
|
|
|
|
const ItemDetail: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const [item, setItem] = useState<Item | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedImage, setSelectedImage] = useState(0);
|
|
const [isAlreadyRenting, setIsAlreadyRenting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchItem();
|
|
if (user) {
|
|
checkIfAlreadyRenting();
|
|
}
|
|
}, [id, user]);
|
|
|
|
const fetchItem = async () => {
|
|
try {
|
|
const response = await itemAPI.getItem(id!);
|
|
setItem(response.data);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Failed to fetch item');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const checkIfAlreadyRenting = async () => {
|
|
try {
|
|
const response = await rentalAPI.getMyRentals();
|
|
const rentals: Rental[] = response.data;
|
|
// Check if user has an active rental for this item
|
|
const hasActiveRental = rentals.some(rental =>
|
|
rental.item?.id === id &&
|
|
['pending', 'confirmed', 'active'].includes(rental.status)
|
|
);
|
|
setIsAlreadyRenting(hasActiveRental);
|
|
} catch (err) {
|
|
console.error('Failed to check rental status:', err);
|
|
}
|
|
};
|
|
|
|
const handleEdit = () => {
|
|
navigate(`/items/${id}/edit`);
|
|
};
|
|
|
|
const handleRent = () => {
|
|
navigate(`/items/${id}/rent`);
|
|
};
|
|
|
|
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 (error || !item) {
|
|
return (
|
|
<div className="container mt-5">
|
|
<div className="alert alert-danger" role="alert">
|
|
{error || 'Item not found'}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isOwner = user?.id === item.ownerId;
|
|
|
|
return (
|
|
<div className="container mt-5">
|
|
<div className="row justify-content-center">
|
|
<div className="col-md-10">
|
|
{item.images.length > 0 ? (
|
|
<div className="mb-4">
|
|
<img
|
|
src={item.images[selectedImage]}
|
|
alt={item.name}
|
|
className="img-fluid rounded mb-3"
|
|
style={{ width: '100%', maxHeight: '500px', objectFit: 'cover' }}
|
|
/>
|
|
{item.images.length > 1 && (
|
|
<div className="d-flex gap-2 overflow-auto justify-content-center">
|
|
{item.images.map((image, index) => (
|
|
<img
|
|
key={index}
|
|
src={image}
|
|
alt={`${item.name} ${index + 1}`}
|
|
className={`rounded cursor-pointer ${selectedImage === index ? 'border border-primary' : ''}`}
|
|
style={{ width: '80px', height: '80px', objectFit: 'cover', cursor: 'pointer' }}
|
|
onClick={() => setSelectedImage(index)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="bg-light rounded d-flex align-items-center justify-content-center mb-4" style={{ height: '400px' }}>
|
|
<span className="text-muted">No image available</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="row">
|
|
<div className="col-md-8">
|
|
<h1>{item.name}</h1>
|
|
<p className="text-muted">{item.location}</p>
|
|
{item.owner && (
|
|
<div
|
|
className="d-flex align-items-center mt-2 mb-3"
|
|
onClick={() => navigate(`/users/${item.ownerId}`)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
{item.owner.profileImage ? (
|
|
<img
|
|
src={item.owner.profileImage}
|
|
alt={`${item.owner.firstName} ${item.owner.lastName}`}
|
|
className="rounded-circle me-2"
|
|
style={{ width: '30px', height: '30px', objectFit: 'cover' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-2"
|
|
style={{ width: '30px', height: '30px' }}
|
|
>
|
|
<i className="bi bi-person-fill text-white" style={{ fontSize: '0.8rem' }}></i>
|
|
</div>
|
|
)}
|
|
<span className="text-muted">{item.owner.firstName} {item.owner.lastName}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-3">
|
|
{item.tags.map((tag, index) => (
|
|
<span key={index} className="badge bg-secondary me-2">{tag}</span>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h5>Description</h5>
|
|
<p>{item.description}</p>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h5>Pricing</h5>
|
|
<div className="row">
|
|
{item.pricePerHour && (
|
|
<div className="col-6">
|
|
<strong>Per Hour:</strong> ${item.pricePerHour}
|
|
</div>
|
|
)}
|
|
{item.pricePerDay && (
|
|
<div className="col-6">
|
|
<strong>Per Day:</strong> ${item.pricePerDay}
|
|
</div>
|
|
)}
|
|
{item.pricePerWeek && (
|
|
<div className="col-6">
|
|
<strong>Per Week:</strong> ${item.pricePerWeek}
|
|
</div>
|
|
)}
|
|
{item.pricePerMonth && (
|
|
<div className="col-6">
|
|
<strong>Per Month:</strong> ${item.pricePerMonth}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h5>Details</h5>
|
|
<p><strong>Replacement Cost:</strong> ${item.replacementCost}</p>
|
|
{item.minimumRentalDays && (
|
|
<p><strong>Minimum Rental:</strong> {item.minimumRentalDays} days</p>
|
|
)}
|
|
{item.maximumRentalDays && (
|
|
<p><strong>Maximum Rental:</strong> {item.maximumRentalDays} days</p>
|
|
)}
|
|
</div>
|
|
|
|
<LocationMap
|
|
latitude={item.latitude}
|
|
longitude={item.longitude}
|
|
location={item.location}
|
|
itemName={item.name}
|
|
/>
|
|
|
|
{item.rules && (
|
|
<div className="mb-4">
|
|
<h5>Rules</h5>
|
|
<p>{item.rules}</p>
|
|
</div>
|
|
)}
|
|
|
|
<ItemReviews itemId={item.id} />
|
|
|
|
<div className="d-flex gap-2">
|
|
{isOwner ? (
|
|
<button className="btn btn-primary" onClick={handleEdit}>
|
|
Edit Listing
|
|
</button>
|
|
) : (
|
|
item.availability && !isAlreadyRenting && (
|
|
<button className="btn btn-primary" onClick={handleRent}>
|
|
Rent This Item
|
|
</button>
|
|
)
|
|
)}
|
|
{!isOwner && isAlreadyRenting && (
|
|
<button className="btn btn-success" disabled style={{ opacity: 0.8 }}>
|
|
✓ Renting
|
|
</button>
|
|
)}
|
|
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
|
|
Back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ItemDetail; |