795 lines
28 KiB
TypeScript
795 lines
28 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 { getPublicImageUrl } from "../services/uploadService";
|
|
import GoogleMapWithRadius from "../components/GoogleMapWithRadius";
|
|
import ItemReviews from "../components/ItemReviews";
|
|
import ConfirmationModal from "../components/ConfirmationModal";
|
|
import Avatar from "../components/Avatar";
|
|
|
|
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);
|
|
const [rentalDates, setRentalDates] = useState({
|
|
startDate: "",
|
|
startTime: "14:00",
|
|
endDate: "",
|
|
endTime: "12:00",
|
|
});
|
|
const [totalCost, setTotalCost] = useState(0);
|
|
const [costLoading, setCostLoading] = useState(false);
|
|
const [costError, setCostError] = useState<string | null>(null);
|
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
const [confirmAction, setConfirmAction] = useState<
|
|
"delete" | "restore" | null
|
|
>(null);
|
|
const [deletionReason, setDeletionReason] = useState("");
|
|
|
|
useEffect(() => {
|
|
fetchItem();
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
checkIfAlreadyRenting();
|
|
} else {
|
|
setIsAlreadyRenting(false);
|
|
}
|
|
}, [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.getRentals();
|
|
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 handleAdminSoftDelete = () => {
|
|
setConfirmAction("delete");
|
|
setShowConfirmModal(true);
|
|
};
|
|
|
|
const handleAdminRestore = () => {
|
|
setConfirmAction("restore");
|
|
setShowConfirmModal(true);
|
|
};
|
|
|
|
const handleConfirmAction = async () => {
|
|
try {
|
|
setDeleteLoading(true);
|
|
setDeleteError(null);
|
|
|
|
if (confirmAction === "delete") {
|
|
await itemAPI.adminSoftDeleteItem(id!, deletionReason);
|
|
} else if (confirmAction === "restore") {
|
|
await itemAPI.adminRestoreItem(id!);
|
|
}
|
|
|
|
await fetchItem(); // Refresh the item to show updated status
|
|
setShowConfirmModal(false);
|
|
setConfirmAction(null);
|
|
setDeletionReason("");
|
|
} catch (err: any) {
|
|
const errorMessage =
|
|
err.response?.data?.error || `Failed to ${confirmAction} item`;
|
|
setDeleteError(errorMessage);
|
|
console.error(`Admin ${confirmAction} failed:`, err);
|
|
setShowConfirmModal(false);
|
|
setConfirmAction(null);
|
|
setDeletionReason("");
|
|
} finally {
|
|
setDeleteLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancelConfirm = () => {
|
|
setShowConfirmModal(false);
|
|
setConfirmAction(null);
|
|
setDeletionReason("");
|
|
};
|
|
|
|
const handleRent = () => {
|
|
const params = new URLSearchParams({
|
|
startDate: rentalDates.startDate,
|
|
startTime: rentalDates.startTime,
|
|
endDate: rentalDates.endDate,
|
|
endTime: rentalDates.endTime,
|
|
});
|
|
navigate(`/items/${id}/rent?${params.toString()}`);
|
|
};
|
|
|
|
const handleCopyLink = async () => {
|
|
const shareUrl = `${window.location.origin}/items/${item?.id}`;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(shareUrl);
|
|
} catch (err) {
|
|
console.error("Copy to clipboard failed:", err);
|
|
}
|
|
};
|
|
|
|
const handleDateTimeChange = (field: string, value: string) => {
|
|
setRentalDates((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
const calculateTotalCost = async () => {
|
|
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
|
|
setTotalCost(0);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCostLoading(true);
|
|
setCostError(null);
|
|
|
|
const startDateTime = new Date(
|
|
`${rentalDates.startDate}T${rentalDates.startTime}`
|
|
).toISOString();
|
|
|
|
const endDateTime = new Date(
|
|
`${rentalDates.endDate}T${rentalDates.endTime}`
|
|
).toISOString();
|
|
|
|
const response = await rentalAPI.getRentalCostPreview({
|
|
itemId: item.id,
|
|
startDateTime,
|
|
endDateTime,
|
|
});
|
|
|
|
setTotalCost(response.data.baseAmount);
|
|
} catch (err: any) {
|
|
setCostError(err.response?.data?.error || "Failed to calculate cost");
|
|
setTotalCost(0);
|
|
} finally {
|
|
setCostLoading(false);
|
|
}
|
|
};
|
|
|
|
const generateTimeOptions = (item: Item | null, selectedDate: string) => {
|
|
const options = [];
|
|
let availableAfter = "00:00";
|
|
let availableBefore = "23:59";
|
|
|
|
// Determine time constraints only if we have both item and a valid selected date
|
|
if (item && selectedDate && selectedDate.trim() !== "") {
|
|
const date = new Date(selectedDate);
|
|
const dayName = date
|
|
.toLocaleDateString("en-US", { weekday: "long" })
|
|
.toLowerCase() as
|
|
| "sunday"
|
|
| "monday"
|
|
| "tuesday"
|
|
| "wednesday"
|
|
| "thursday"
|
|
| "friday"
|
|
| "saturday";
|
|
|
|
// Use day-specific times if available
|
|
if (
|
|
item.specifyTimesPerDay &&
|
|
item.weeklyTimes &&
|
|
item.weeklyTimes[dayName]
|
|
) {
|
|
const dayTimes = item.weeklyTimes[dayName];
|
|
availableAfter = dayTimes.availableAfter;
|
|
availableBefore = dayTimes.availableBefore;
|
|
}
|
|
// Otherwise use global times
|
|
else if (item.availableAfter && item.availableBefore) {
|
|
availableAfter = item.availableAfter;
|
|
availableBefore = item.availableBefore;
|
|
}
|
|
}
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
const time24 = `${hour.toString().padStart(2, "0")}:00`;
|
|
|
|
// Ensure consistent format for comparison (normalize to HH:MM)
|
|
const normalizedAvailableAfter =
|
|
availableAfter.length === 5 ? availableAfter : availableAfter + ":00";
|
|
const normalizedAvailableBefore =
|
|
availableBefore.length === 5
|
|
? availableBefore
|
|
: availableBefore + ":00";
|
|
|
|
// Check if this time is within the available range
|
|
if (
|
|
time24 >= normalizedAvailableAfter &&
|
|
time24 <= normalizedAvailableBefore
|
|
) {
|
|
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
const period = hour < 12 ? "AM" : "PM";
|
|
const time12 = `${hour12}:00 ${period}`;
|
|
options.push({ value: time24, label: time12 });
|
|
}
|
|
}
|
|
|
|
// If no options are available, return at least one option to prevent empty dropdown
|
|
if (options.length === 0) {
|
|
options.push({ value: "00:00", label: "Not Available" });
|
|
}
|
|
|
|
return options;
|
|
};
|
|
|
|
useEffect(() => {
|
|
calculateTotalCost();
|
|
}, [rentalDates, item]);
|
|
|
|
// Validate and adjust selected times based on item availability
|
|
useEffect(() => {
|
|
if (!item) return;
|
|
|
|
const validateAndAdjustTime = (date: string, currentTime: string) => {
|
|
if (!date) return currentTime;
|
|
|
|
const availableOptions = generateTimeOptions(item, date);
|
|
if (availableOptions.length === 0) return currentTime;
|
|
|
|
// If current time is not in available options, use the first available time
|
|
const isCurrentTimeValid = availableOptions.some(
|
|
(option) => option.value === currentTime
|
|
);
|
|
return isCurrentTimeValid ? currentTime : availableOptions[0].value;
|
|
};
|
|
|
|
const adjustedStartTime = validateAndAdjustTime(
|
|
rentalDates.startDate,
|
|
rentalDates.startTime
|
|
);
|
|
const adjustedEndTime = validateAndAdjustTime(
|
|
rentalDates.endDate || rentalDates.startDate,
|
|
rentalDates.endTime
|
|
);
|
|
|
|
// Update state if times have changed
|
|
if (
|
|
adjustedStartTime !== rentalDates.startTime ||
|
|
adjustedEndTime !== rentalDates.endTime
|
|
) {
|
|
setRentalDates((prev) => ({
|
|
...prev,
|
|
startTime: adjustedStartTime,
|
|
endTime: adjustedEndTime,
|
|
}));
|
|
}
|
|
}, [item, rentalDates.startDate, rentalDates.endDate]);
|
|
|
|
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;
|
|
const isAdmin = user?.role === "admin";
|
|
|
|
return (
|
|
<div className="container mt-5">
|
|
<div className="row justify-content-center">
|
|
<div className="col-md-10">
|
|
{/* Deleted Status Indicator for Admins */}
|
|
{item.isDeleted && isAdmin && (
|
|
<div className="alert alert-warning mb-3" role="alert">
|
|
<i className="bi bi-exclamation-triangle-fill me-2"></i>
|
|
<strong>Item Soft Deleted</strong> - This item is hidden from
|
|
public listings.
|
|
{item.deleter && (
|
|
<span className="ms-2">
|
|
Deleted by {item.deleter.firstName} {item.deleter.lastName}
|
|
</span>
|
|
)}
|
|
{item.deletedAt && (
|
|
<span className="ms-2">
|
|
on {new Date(item.deletedAt).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
{item.deletionReason && (
|
|
<div className="mt-2">
|
|
<strong>Reason:</strong> {item.deletionReason}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Error Alert */}
|
|
{deleteError && (
|
|
<div className="alert alert-danger mb-3" role="alert">
|
|
{deleteError}
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons (Owner Edit + Admin Soft Delete/Restore) */}
|
|
{(isOwner || isAdmin) && (
|
|
<div className="d-flex justify-content-end gap-2 mb-3">
|
|
{isOwner && (
|
|
<button
|
|
className="btn btn-outline-primary"
|
|
onClick={handleEdit}
|
|
>
|
|
<i className="bi bi-pencil me-2"></i>
|
|
Edit Listing
|
|
</button>
|
|
)}
|
|
{isAdmin && !item.isDeleted && (
|
|
<button
|
|
className="btn btn-outline-danger"
|
|
onClick={handleAdminSoftDelete}
|
|
disabled={deleteLoading}
|
|
>
|
|
{deleteLoading ? (
|
|
<>
|
|
<span
|
|
className="spinner-border spinner-border-sm me-2"
|
|
role="status"
|
|
aria-hidden="true"
|
|
></span>
|
|
Deleting...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-trash me-2"></i>
|
|
Delete
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
{isAdmin && item.isDeleted && (
|
|
<button
|
|
className="btn btn-outline-success"
|
|
onClick={handleAdminRestore}
|
|
disabled={deleteLoading}
|
|
>
|
|
{deleteLoading ? (
|
|
<>
|
|
<span
|
|
className="spinner-border spinner-border-sm me-2"
|
|
role="status"
|
|
aria-hidden="true"
|
|
></span>
|
|
Restoring...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-arrow-counterclockwise me-2"></i>
|
|
Restore
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="row">
|
|
<div className="col-md-8">
|
|
{/* Images */}
|
|
{item.imageFilenames.length > 0 ? (
|
|
<div className="mb-4">
|
|
<img
|
|
src={getPublicImageUrl(item.imageFilenames[selectedImage])}
|
|
alt={item.name}
|
|
className="img-fluid rounded mb-3"
|
|
style={{
|
|
width: "100%",
|
|
maxHeight: "500px",
|
|
objectFit: "contain",
|
|
backgroundColor: "#f8f9fa",
|
|
}}
|
|
/>
|
|
{item.imageFilenames.length > 1 && (
|
|
<div className="d-flex gap-2 overflow-auto justify-content-center">
|
|
{item.imageFilenames.map((image, index) => (
|
|
<img
|
|
key={index}
|
|
src={getPublicImageUrl(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>
|
|
)}
|
|
{/* Item Name */}
|
|
<div className="d-flex align-items-center justify-content-between mb-3">
|
|
<h1 className="mb-0">{item.name}</h1>
|
|
<button
|
|
className="btn btn-outline-secondary btn-sm"
|
|
onClick={handleCopyLink}
|
|
aria-label="Copy link to this item"
|
|
>
|
|
<i className="bi bi-link-45deg me-2"></i>
|
|
Copy Link
|
|
</button>
|
|
</div>
|
|
|
|
{/* Owner Info */}
|
|
{item.owner && (
|
|
<div
|
|
className="d-flex align-items-center mb-4"
|
|
onClick={() => navigate(`/users/${item.ownerId}`)}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
<Avatar user={item.owner} size="xs" className="me-2" />
|
|
<span className="text-muted">
|
|
{item.owner.firstName} {item.owner.lastName}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description (no label) */}
|
|
<div className="mb-4">
|
|
<p>{item.description}</p>
|
|
</div>
|
|
|
|
{/* Map */}
|
|
<GoogleMapWithRadius
|
|
latitude={item.latitude}
|
|
longitude={item.longitude}
|
|
/>
|
|
|
|
<ItemReviews itemId={item.id} />
|
|
|
|
{/* Rules */}
|
|
{item.rules && (
|
|
<div className="mb-4">
|
|
<h5>Rules</h5>
|
|
<p>{item.rules}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cancellation Policy */}
|
|
<div className="mb-4">
|
|
<h5>Cancellation Policy</h5>
|
|
<div className="small">
|
|
<div className="mb-2">
|
|
Full refund: Cancel 48+ hours before rental start time
|
|
</div>
|
|
<div className="mb-2">
|
|
50% refund: Cancel 24-48 hours before rental start time
|
|
</div>
|
|
<div className="mb-2">
|
|
No refund: Cancel within 24 hours of rental start time
|
|
</div>
|
|
<div>Replacement Cost: ${item.replacementCost}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side - Sticky Pricing Card */}
|
|
<div className="col-md-4">
|
|
<div
|
|
className="card sticky-pricing-card"
|
|
style={{
|
|
position: "sticky",
|
|
top: "20px",
|
|
alignSelf: "flex-start",
|
|
}}
|
|
>
|
|
<div className="card-body text-center">
|
|
{(() => {
|
|
const hasAnyPositivePrice =
|
|
(item.pricePerHour !== undefined &&
|
|
Number(item.pricePerHour) > 0) ||
|
|
(item.pricePerDay !== undefined &&
|
|
Number(item.pricePerDay) > 0) ||
|
|
(item.pricePerWeek !== undefined &&
|
|
Number(item.pricePerWeek) > 0) ||
|
|
(item.pricePerMonth !== undefined &&
|
|
Number(item.pricePerMonth) > 0);
|
|
|
|
const hasAnyZeroPrice =
|
|
(item.pricePerHour !== undefined &&
|
|
Number(item.pricePerHour) === 0) ||
|
|
(item.pricePerDay !== undefined &&
|
|
Number(item.pricePerDay) === 0) ||
|
|
(item.pricePerWeek !== undefined &&
|
|
Number(item.pricePerWeek) === 0) ||
|
|
(item.pricePerMonth !== undefined &&
|
|
Number(item.pricePerMonth) === 0);
|
|
|
|
if (!hasAnyPositivePrice && hasAnyZeroPrice) {
|
|
return (
|
|
<div className="mb-4">
|
|
<h4>Free to Borrow</h4>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{item.pricePerHour !== undefined &&
|
|
Number(item.pricePerHour) > 0 && (
|
|
<div className="mb-2">
|
|
<h4>
|
|
${Math.floor(Number(item.pricePerHour))}/Hour
|
|
</h4>
|
|
</div>
|
|
)}
|
|
{item.pricePerDay !== undefined &&
|
|
Number(item.pricePerDay) > 0 && (
|
|
<div className="mb-2">
|
|
<h4>
|
|
${Math.floor(Number(item.pricePerDay))}/Day
|
|
</h4>
|
|
</div>
|
|
)}
|
|
{item.pricePerWeek !== undefined &&
|
|
Number(item.pricePerWeek) > 0 && (
|
|
<div className="mb-2">
|
|
<h4>
|
|
${Math.floor(Number(item.pricePerWeek))}/Week
|
|
</h4>
|
|
</div>
|
|
)}
|
|
{item.pricePerMonth !== undefined &&
|
|
Number(item.pricePerMonth) > 0 && (
|
|
<div className="mb-2">
|
|
<h4>
|
|
${Math.floor(Number(item.pricePerMonth))}/Month
|
|
</h4>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
})()}
|
|
|
|
{/* Rental Period Selection - Only show for non-owners */}
|
|
{!isOwner && item.isAvailable && !isAlreadyRenting && (
|
|
<>
|
|
<hr />
|
|
<div className="text-start">
|
|
<div className="mb-2">
|
|
<label className="form-label small mb-1">Start</label>
|
|
<div className="input-group input-group-sm">
|
|
<input
|
|
type="date"
|
|
className="form-control"
|
|
value={rentalDates.startDate}
|
|
onChange={(e) =>
|
|
handleDateTimeChange(
|
|
"startDate",
|
|
e.target.value
|
|
)
|
|
}
|
|
min={new Date().toLocaleDateString()}
|
|
style={{ flex: "1 1 50%" }}
|
|
/>
|
|
<select
|
|
className="form-select"
|
|
value={rentalDates.startTime}
|
|
onChange={(e) =>
|
|
handleDateTimeChange(
|
|
"startTime",
|
|
e.target.value
|
|
)
|
|
}
|
|
style={{ flex: "1 1 50%" }}
|
|
disabled={
|
|
!!(
|
|
rentalDates.startDate &&
|
|
generateTimeOptions(
|
|
item,
|
|
rentalDates.startDate
|
|
).every(
|
|
(opt) => opt.label === "Not Available"
|
|
)
|
|
)
|
|
}
|
|
>
|
|
{generateTimeOptions(
|
|
item,
|
|
rentalDates.startDate
|
|
).map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-3">
|
|
<label className="form-label small mb-1">End</label>
|
|
<div className="input-group input-group-sm">
|
|
<input
|
|
type="date"
|
|
className="form-control"
|
|
value={rentalDates.endDate}
|
|
onChange={(e) =>
|
|
handleDateTimeChange("endDate", e.target.value)
|
|
}
|
|
min={
|
|
rentalDates.startDate ||
|
|
new Date().toLocaleDateString()
|
|
}
|
|
style={{ flex: "1 1 50%" }}
|
|
/>
|
|
<select
|
|
className="form-select"
|
|
value={rentalDates.endTime}
|
|
onChange={(e) =>
|
|
handleDateTimeChange("endTime", e.target.value)
|
|
}
|
|
style={{ flex: "1 1 50%" }}
|
|
disabled={
|
|
!!(
|
|
(rentalDates.endDate ||
|
|
rentalDates.startDate) &&
|
|
generateTimeOptions(
|
|
item,
|
|
rentalDates.endDate || rentalDates.startDate
|
|
).every(
|
|
(opt) => opt.label === "Not Available"
|
|
)
|
|
)
|
|
}
|
|
>
|
|
{generateTimeOptions(
|
|
item,
|
|
rentalDates.endDate || rentalDates.startDate
|
|
).map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{rentalDates.startDate && rentalDates.endDate && (
|
|
<div className="mb-3 p-2 bg-light rounded text-center">
|
|
{costLoading ? (
|
|
<div
|
|
className="spinner-border spinner-border-sm"
|
|
role="status"
|
|
>
|
|
<span className="visually-hidden">
|
|
Calculating...
|
|
</span>
|
|
</div>
|
|
) : costError ? (
|
|
<small className="text-danger">{costError}</small>
|
|
) : totalCost > 0 ? (
|
|
<strong>Total: ${totalCost}</strong>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
{!isOwner && item.isAvailable && !isAlreadyRenting && (
|
|
<div className="d-grid">
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleRent}
|
|
disabled={
|
|
!rentalDates.startDate || !rentalDates.endDate
|
|
}
|
|
>
|
|
Rent Now
|
|
</button>
|
|
</div>
|
|
)}
|
|
{!isOwner && isAlreadyRenting && (
|
|
<div className="d-grid">
|
|
<button
|
|
className="btn btn-success"
|
|
disabled
|
|
style={{ opacity: 0.8 }}
|
|
>
|
|
✓ Renting
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confirmation Modal */}
|
|
<ConfirmationModal
|
|
show={showConfirmModal}
|
|
onClose={handleCancelConfirm}
|
|
onConfirm={handleConfirmAction}
|
|
title={
|
|
confirmAction === "delete" ? "Confirm Delete" : "Confirm Restore"
|
|
}
|
|
message={
|
|
confirmAction === "delete"
|
|
? "Are you sure you want to delete this item? It will be hidden from public listings."
|
|
: "Are you sure you want to restore this item? It will be visible to the public again."
|
|
}
|
|
confirmText={confirmAction === "delete" ? "Delete" : "Restore"}
|
|
cancelText="Cancel"
|
|
confirmButtonClass={
|
|
confirmAction === "delete" ? "btn-danger" : "btn-success"
|
|
}
|
|
loading={deleteLoading}
|
|
showReasonInput={confirmAction === "delete"}
|
|
reason={deletionReason}
|
|
onReasonChange={setDeletionReason}
|
|
reasonPlaceholder="Enter reason for deletion (e.g., policy violation, inappropriate content)"
|
|
reasonRequired={true}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ItemDetail;
|