getting to payment screen. Bug fixes and formatting changes for item detail

This commit is contained in:
jackiettran
2025-08-21 16:44:05 -04:00
parent b624841350
commit 022c0b9c06
10 changed files with 859 additions and 815 deletions

View File

@@ -1,127 +1,123 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const { DataTypes } = require("sequelize");
const sequelize = require("../config/database");
const Item = sequelize.define('Item', {
const Item = sequelize.define("Item", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
primaryKey: true,
},
name: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: false
allowNull: false,
},
pickUpAvailable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
localDeliveryAvailable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
localDeliveryRadius: {
type: DataTypes.INTEGER,
validate: {
min: 1,
max: 100
}
max: 100,
},
},
shippingAvailable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
inPlaceUseAvailable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
pricePerHour: {
type: DataTypes.DECIMAL(10, 2)
type: DataTypes.DECIMAL(10, 2),
},
pricePerDay: {
type: DataTypes.DECIMAL(10, 2)
type: DataTypes.DECIMAL(10, 2),
},
replacementCost: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
location: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
address1: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
address2: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
city: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
state: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
zipCode: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
country: {
type: DataTypes.STRING
type: DataTypes.STRING,
},
latitude: {
type: DataTypes.DECIMAL(10, 8)
type: DataTypes.DECIMAL(10, 8),
},
longitude: {
type: DataTypes.DECIMAL(11, 8)
type: DataTypes.DECIMAL(11, 8),
},
images: {
type: DataTypes.ARRAY(DataTypes.STRING),
defaultValue: []
defaultValue: [],
},
availability: {
type: DataTypes.BOOLEAN,
defaultValue: true
defaultValue: true,
},
specifications: {
type: DataTypes.JSONB,
defaultValue: {}
defaultValue: {},
},
rules: {
type: DataTypes.TEXT
type: DataTypes.TEXT,
},
minimumRentalDays: {
type: DataTypes.INTEGER,
defaultValue: 1
defaultValue: 1,
},
maximumRentalDays: {
type: DataTypes.INTEGER
type: DataTypes.INTEGER,
},
needsTraining: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
defaultValue: false,
},
unavailablePeriods: {
type: DataTypes.JSONB,
defaultValue: []
defaultValue: [],
},
availableAfter: {
type: DataTypes.STRING,
defaultValue: '09:00'
defaultValue: "09:00",
},
availableBefore: {
type: DataTypes.STRING,
defaultValue: '17:00'
defaultValue: "17:00",
},
specifyTimesPerDay: {
type: DataTypes.BOOLEAN,
defaultValue: false
defaultValue: false,
},
weeklyTimes: {
type: DataTypes.JSONB,
@@ -132,17 +128,17 @@ const Item = sequelize.define('Item', {
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
friday: { availableAfter: "09:00", availableBefore: "17:00" },
saturday: { availableAfter: "09:00", availableBefore: "17:00" }
}
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
},
},
ownerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
}
model: "Users",
key: "id",
},
},
});
module.exports = Item;

View File

@@ -9,7 +9,6 @@ router.get("/", async (req, res) => {
const {
minPrice,
maxPrice,
location,
city,
zipCode,
search,
@@ -24,7 +23,6 @@ router.get("/", async (req, res) => {
if (minPrice) where.pricePerDay[Op.gte] = minPrice;
if (maxPrice) where.pricePerDay[Op.lte] = maxPrice;
}
if (location) where.location = { [Op.iLike]: `%${location}%` };
if (city) where.city = { [Op.iLike]: `%${city}%` };
if (zipCode) where.zipCode = { [Op.iLike]: `%${zipCode}%` };
if (search) {
@@ -83,6 +81,42 @@ router.get("/recommendations", authenticateToken, async (req, res) => {
}
});
// Public endpoint to get reviews for a specific item (must come before /:id route)
router.get('/:id/reviews', async (req, res) => {
try {
const { Rental, User } = require('../models');
const reviews = await Rental.findAll({
where: {
itemId: req.params.id,
status: 'completed',
rating: { [Op.not]: null },
review: { [Op.not]: null }
},
include: [
{
model: User,
as: 'renter',
attributes: ['id', 'firstName', 'lastName']
}
],
order: [['createdAt', 'DESC']]
});
const averageRating = reviews.length > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
: 0;
res.json({
reviews,
averageRating,
totalReviews: reviews.length
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.get("/:id", async (req, res) => {
try {
const item = await Item.findByPk(req.params.id, {

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Item } from '../types';
interface ItemCardProps {
item: Item;
variant?: 'compact' | 'standard';
}
const ItemCard: React.FC<ItemCardProps> = ({
item,
variant = 'standard'
}) => {
const isCompact = variant === 'compact';
const getPriceDisplay = () => {
if (item.pricePerDay !== undefined) {
return Number(item.pricePerDay) === 0
? "Free to Borrow"
: `$${Math.floor(Number(item.pricePerDay))}/Day`;
} else if (item.pricePerHour !== undefined) {
return Number(item.pricePerHour) === 0
? "Free to Borrow"
: `$${Math.floor(Number(item.pricePerHour))}/Hour`;
}
return null;
};
const getLocationDisplay = () => {
return item.city && item.state
? `${item.city}, ${item.state}`
: item.location;
};
return (
<Link to={`/items/${item.id}`} className="text-decoration-none">
<div className="card h-100" style={{ cursor: 'pointer' }}>
{item.images && item.images[0] ? (
<img
src={item.images[0]}
className="card-img-top"
alt={item.name}
style={{
height: isCompact ? '150px' : '200px',
objectFit: 'cover'
}}
/>
) : (
<div
className="card-img-top bg-light d-flex align-items-center justify-content-center"
style={{ height: isCompact ? '150px' : '200px' }}
>
<i className="bi bi-image text-muted" style={{ fontSize: '2rem' }}></i>
</div>
)}
<div className={`card-body ${isCompact ? 'p-2' : ''}`}>
{isCompact ? (
<h6 className="card-title text-truncate mb-1 text-dark">{item.name}</h6>
) : (
<h5 className="card-title text-dark">{item.name}</h5>
)}
<div className={isCompact ? "mb-1" : "mb-3"}>
<div className="text-primary">
<strong className={isCompact ? "small" : ""}>
{getPriceDisplay()}
</strong>
</div>
</div>
<div className={`text-muted small ${isCompact ? 'mb-1' : 'mb-2'}`}>
<i className="bi bi-geo-alt"></i> {getLocationDisplay()}
</div>
</div>
</div>
</Link>
);
};
export default ItemCard;

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { Rental } from "../types";
import { rentalAPI } from "../services/api";
import { itemAPI } from "../services/api";
interface ItemReviewsProps {
itemId: string;
@@ -17,26 +17,11 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
const fetchReviews = async () => {
try {
// Fetch all rentals for this item
const response = await rentalAPI.getMyListings();
const allRentals: Rental[] = response.data;
const response = await itemAPI.getItemReviews(itemId);
const { reviews, averageRating } = response.data;
// Filter for completed rentals with reviews for this specific item
const itemReviews = allRentals.filter(
(rental) =>
rental.itemId === itemId &&
rental.status === "completed" &&
rental.rating &&
rental.review
);
setReviews(itemReviews);
// Calculate average rating
if (itemReviews.length > 0) {
const sum = itemReviews.reduce((acc, r) => acc + (r.rating || 0), 0);
setAverageRating(sum / itemReviews.length);
}
setReviews(reviews);
setAverageRating(averageRating);
} catch (error) {
console.error("Failed to fetch reviews:", error);
} finally {

View File

@@ -19,7 +19,6 @@ interface ItemFormData {
pricePerHour?: number | string;
pricePerDay?: number | string;
replacementCost: number | string;
location: string;
address1: string;
address2: string;
city: string;
@@ -57,7 +56,6 @@ const CreateItem: React.FC = () => {
inPlaceUseAvailable: false,
pricePerDay: "",
replacementCost: "",
location: "",
address1: "",
address2: "",
city: "",

View File

@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { itemAPI } from '../services/api';
import { Item } from '../types';
import ItemCard from '../components/ItemCard';
const Home: React.FC = () => {
const { user } = useAuth();
@@ -80,43 +81,7 @@ const Home: React.FC = () => {
<div className="row g-4">
{featuredItems.map((item) => (
<div key={item.id} className="col-md-6 col-lg-4 col-xl-3">
<Link to={`/items/${item.id}`} className="text-decoration-none">
<div className="card h-100 shadow-sm hover-shadow">
{item.images && item.images.length > 0 ? (
<img
src={item.images[0]}
className="card-img-top"
alt={item.name}
style={{ height: '150px', objectFit: 'cover' }}
/>
) : (
<div
className="card-img-top bg-light d-flex align-items-center justify-content-center"
style={{ height: '150px' }}
>
<i className="bi bi-image text-muted" style={{ fontSize: '2rem' }}></i>
</div>
)}
<div className="card-body p-2">
<h6 className="card-title text-truncate mb-1">{item.name}</h6>
<p className="card-text text-muted small text-truncate mb-1">
{item.location}
</p>
<div className="d-flex justify-content-between align-items-center">
<div>
{item.pricePerDay && (
<div className="fw-bold small">
${item.pricePerDay}/day
</div>
)}
</div>
<small className="text-muted" style={{ fontSize: '0.75rem' }}>
{item.owner?.firstName || 'Unknown'}
</small>
</div>
</div>
</div>
</Link>
<ItemCard item={item} variant="compact" />
</div>
))}
</div>

View File

@@ -1,10 +1,10 @@
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';
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 }>();
@@ -15,6 +15,13 @@ const ItemDetail: React.FC = () => {
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);
useEffect(() => {
fetchItem();
@@ -23,6 +30,8 @@ const ItemDetail: React.FC = () => {
useEffect(() => {
if (user) {
checkIfAlreadyRenting();
} else {
setIsAlreadyRenting(false);
}
}, [id, user]);
@@ -31,7 +40,7 @@ const ItemDetail: React.FC = () => {
const response = await itemAPI.getItem(id!);
setItem(response.data);
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to fetch item');
setError(err.response?.data?.message || "Failed to fetch item");
} finally {
setLoading(false);
}
@@ -42,13 +51,14 @@ const ItemDetail: React.FC = () => {
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 =>
const hasActiveRental = rentals.some(
(rental) =>
rental.item?.id === id &&
['pending', 'confirmed', 'active'].includes(rental.status)
["pending", "confirmed", "active"].includes(rental.status)
);
setIsAlreadyRenting(hasActiveRental);
} catch (err) {
console.error('Failed to check rental status:', err);
console.error("Failed to check rental status:", err);
}
};
@@ -57,9 +67,65 @@ const ItemDetail: React.FC = () => {
};
const handleRent = () => {
navigate(`/items/${id}/rent`);
const params = new URLSearchParams({
startDate: rentalDates.startDate,
startTime: rentalDates.startTime,
endDate: rentalDates.endDate,
endTime: rentalDates.endTime
});
navigate(`/items/${id}/rent?${params.toString()}`);
};
const handleDateTimeChange = (field: string, value: string) => {
setRentalDates((prev) => ({
...prev,
[field]: value,
}));
};
const calculateTotalCost = () => {
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
setTotalCost(0);
return;
}
const startDateTime = new Date(
`${rentalDates.startDate}T${rentalDates.startTime}`
);
const endDateTime = new Date(
`${rentalDates.endDate}T${rentalDates.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);
};
const generateTimeOptions = () => {
const options = [];
for (let hour = 0; hour < 24; hour++) {
const time24 = `${hour.toString().padStart(2, "0")}:00`;
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 });
}
return options;
};
useEffect(() => {
calculateTotalCost();
}, [rentalDates, item]);
if (loading) {
return (
<div className="container mt-5">
@@ -76,7 +142,7 @@ const ItemDetail: React.FC = () => {
return (
<div className="container mt-5">
<div className="alert alert-danger" role="alert">
{error || 'Item not found'}
{error || "Item not found"}
</div>
</div>
);
@@ -97,13 +163,20 @@ const ItemDetail: React.FC = () => {
</div>
)}
<div className="row">
<div className="col-md-8">
{/* Images */}
{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' }}
style={{
width: "100%",
maxHeight: "500px",
objectFit: "cover",
}}
/>
{item.images.length > 1 && (
<div className="d-flex gap-2 overflow-auto justify-content-center">
@@ -112,8 +185,17 @@ const ItemDetail: React.FC = () => {
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' }}
className={`rounded cursor-pointer ${
selectedImage === index
? "border border-primary"
: ""
}`}
style={{
width: "80px",
height: "80px",
objectFit: "cover",
cursor: "pointer",
}}
onClick={() => setSelectedImage(index)}
/>
))}
@@ -121,13 +203,13 @@ const ItemDetail: React.FC = () => {
)}
</div>
) : (
<div className="bg-light rounded d-flex align-items-center justify-content-center mb-4" style={{ height: '400px' }}>
<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">
{/* Item Name */}
<h1 className="mb-3">{item.name}</h1>
@@ -136,24 +218,33 @@ const ItemDetail: React.FC = () => {
<div
className="d-flex align-items-center mb-4"
onClick={() => navigate(`/users/${item.ownerId}`)}
style={{ cursor: 'pointer' }}
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' }}
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' }}
style={{ width: "30px", height: "30px" }}
>
<i className="bi bi-person-fill text-white" style={{ fontSize: '0.8rem' }}></i>
<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>
<span className="text-muted">
{item.owner.firstName} {item.owner.lastName}
</span>
</div>
)}
@@ -161,52 +252,6 @@ const ItemDetail: React.FC = () => {
<div className="mb-4">
<p>{item.description}</p>
</div>
</div>
{/* Right Side - Pricing Card */}
<div className="col-md-4">
<div className="card">
<div className="card-body text-center">
{item.pricePerHour && (
<div className="mb-2">
<h4>${Math.floor(item.pricePerHour)}/Hour</h4>
</div>
)}
{item.pricePerDay && (
<div className="mb-2">
<h4>${Math.floor(item.pricePerDay)}/Day</h4>
</div>
)}
{item.pricePerWeek && (
<div className="mb-2">
<h4>${Math.floor(item.pricePerWeek)}/Week</h4>
</div>
)}
{item.pricePerMonth && (
<div className="mb-4">
<h4>${Math.floor(item.pricePerMonth)}/Month</h4>
</div>
)}
{/* Action Buttons */}
{!isOwner && item.availability && !isAlreadyRenting && (
<div className="d-grid">
<button className="btn btn-primary" onClick={handleRent}>
Rent This Item
</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>
{/* Map */}
<LocationMap
@@ -226,9 +271,209 @@ const ItemDetail: React.FC = () => {
</div>
)}
{/* Replacement Cost (under Rules) */}
{/* Cancellation Policy */}
<div className="mb-4">
<p><strong>Replacement Cost:</strong> ${item.replacementCost}</p>
<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-4">
<h4>
${Math.floor(Number(item.pricePerMonth))}/Month
</h4>
</div>
)}
</>
);
})()}
{/* Rental Period Selection - Only show for non-owners */}
{!isOwner && item.availability && !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().toISOString().split("T")[0]}
style={{ flex: '1 1 50%' }}
/>
<select
className="form-select"
value={rentalDates.startTime}
onChange={(e) =>
handleDateTimeChange("startTime", e.target.value)
}
style={{ flex: '1 1 50%' }}
>
{generateTimeOptions().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().toISOString().split("T")[0]
}
style={{ flex: '1 1 50%' }}
/>
<select
className="form-select"
value={rentalDates.endTime}
onChange={(e) =>
handleDateTimeChange("endTime", e.target.value)
}
style={{ flex: '1 1 50%' }}
>
{generateTimeOptions().map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
{rentalDates.startDate &&
rentalDates.endDate &&
totalCost > 0 && (
<div className="mb-3 p-2 bg-light rounded text-center">
<strong>Total: ${totalCost}</strong>
</div>
)}
</div>
</>
)}
{/* Action Buttons */}
{!isOwner && item.availability && !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>

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { Item } from '../types';
import { itemAPI } from '../services/api';
import React, { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { Item } from "../types";
import { itemAPI } from "../services/api";
import ItemCard from "../components/ItemCard";
const ItemList: React.FC = () => {
const [searchParams] = useSearchParams();
@@ -9,9 +10,9 @@ const ItemList: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState({
search: searchParams.get('search') || '',
city: searchParams.get('city') || '',
zipCode: searchParams.get('zipCode') || ''
search: searchParams.get("search") || "",
city: searchParams.get("city") || "",
zipCode: searchParams.get("zipCode") || "",
});
useEffect(() => {
@@ -21,9 +22,9 @@ const ItemList: React.FC = () => {
// Update filters when URL params change (e.g., from navbar search)
useEffect(() => {
setFilters({
search: searchParams.get('search') || '',
city: searchParams.get('city') || '',
zipCode: searchParams.get('zipCode') || ''
search: searchParams.get("search") || "",
city: searchParams.get("city") || "",
zipCode: searchParams.get("zipCode") || "",
});
}, [searchParams]);
@@ -31,33 +32,32 @@ const ItemList: React.FC = () => {
try {
setLoading(true);
const params = {
...filters
...filters,
};
// Remove empty filters
Object.keys(params).forEach(key => {
Object.keys(params).forEach((key) => {
if (!params[key as keyof typeof params]) {
delete params[key as keyof typeof params];
}
});
const response = await itemAPI.getItems(params);
console.log('API Response:', response);
// Access the items array from response.data.items
const allItems = response.data.items || response.data || [];
// Filter only available items
const availableItems = allItems.filter((item: Item) => item.availability);
setItems(availableItems);
} catch (err: any) {
console.error('Error fetching items:', err);
console.error('Error response:', err.response);
setError(err.response?.data?.message || err.message || 'Failed to fetch items');
console.error("Error fetching items:", err);
console.error("Error response:", err.response);
setError(
err.response?.data?.message || err.message || "Failed to fetch items"
);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="container mt-5">
@@ -94,49 +94,7 @@ const ItemList: React.FC = () => {
<div className="row">
{items.map((item) => (
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
<Link
to={`/items/${item.id}`}
className="text-decoration-none"
>
<div className="card h-100" style={{ cursor: 'pointer' }}>
{item.images && item.images[0] && (
<img
src={item.images[0]}
className="card-img-top"
alt={item.name}
style={{ height: '200px', objectFit: 'cover' }}
/>
)}
<div className="card-body">
<h5 className="card-title text-dark">
{item.name}
</h5>
<p className="card-text text-truncate text-dark">{item.description}</p>
<div className="mb-3">
{item.pricePerDay && (
<div className="text-primary">
<strong>${item.pricePerDay}/day</strong>
</div>
)}
{item.pricePerHour && (
<div className="text-primary">
<strong>${item.pricePerHour}/hour</strong>
</div>
)}
</div>
<div className="mb-2 text-muted small">
<i className="bi bi-geo-alt"></i> {item.location}
</div>
{item.owner && (
<small className="text-muted">by {item.owner.firstName} {item.owner.lastName}</small>
)}
</div>
</div>
</Link>
<ItemCard item={item} variant="standard" />
</div>
))}
</div>

View File

@@ -1,95 +1,89 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Item } from '../types';
import { useAuth } from '../contexts/AuthContext';
import { itemAPI, rentalAPI } from '../services/api';
import AvailabilityCalendar from '../components/AvailabilityCalendar';
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";
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 [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState({
deliveryMethod: 'pickup' as 'pickup' | 'delivery',
deliveryAddress: '',
cardNumber: '',
cardExpiry: '',
cardCVC: '',
cardName: ''
deliveryMethod: "pickup" as "pickup" | "delivery",
deliveryAddress: "",
cardNumber: "",
cardExpiry: "",
cardCVC: "",
cardName: "",
});
const [manualSelection, setManualSelection] = useState({
startDate: '',
startTime: '09:00',
endDate: '',
endTime: '17:00'
startDate: searchParams.get("startDate") || "",
startTime: searchParams.get("startTime") || "09:00",
endDate: searchParams.get("endDate") || "",
endTime: searchParams.get("endTime") || "17:00",
});
const [selectedPeriods, setSelectedPeriods] = useState<Array<{
id: string;
startDate: Date;
endDate: Date;
startTime?: string;
endTime?: string;
}>>([]);
const [totalCost, setTotalCost] = useState(0);
const [rentalDuration, setRentalDuration] = useState({ days: 0, hours: 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(() => {
calculateTotal();
}, [selectedPeriods, item]);
useEffect(() => {
// Sync manual selection with selected periods
if (selectedPeriods.length > 0) {
const period = selectedPeriods[0];
// Extract hours from the Date objects if startTime/endTime not provided
let startTimeStr = period.startTime;
let endTimeStr = period.endTime;
if (!startTimeStr) {
const startHour = period.startDate.getHours();
startTimeStr = `${startHour.toString().padStart(2, '0')}:00`;
}
if (!endTimeStr) {
const endHour = period.endDate.getHours();
// If the end hour is 23:59:59, show it as 00:00 of the next day
if (endHour === 23 && period.endDate.getMinutes() === 59) {
endTimeStr = '00:00';
// Adjust the end date to show the next day
const adjustedEndDate = new Date(period.endDate);
adjustedEndDate.setDate(adjustedEndDate.getDate() + 1);
setManualSelection({
startDate: period.startDate.toISOString().split('T')[0],
startTime: startTimeStr,
endDate: adjustedEndDate.toISOString().split('T')[0],
endTime: endTimeStr
});
return;
} else {
endTimeStr = `${endHour.toString().padStart(2, '0')}:00`;
}
}
setManualSelection({
startDate: period.startDate.toISOString().split('T')[0],
startTime: startTimeStr,
endDate: period.endDate.toISOString().split('T')[0],
endTime: endTimeStr
});
}
}, [selectedPeriods]);
calculateTotalCost();
}, [item, manualSelection]);
const fetchItem = async () => {
try {
@@ -98,61 +92,20 @@ const RentItem: React.FC = () => {
// Check if item is available
if (!response.data.availability) {
setError('This item is not available for rent');
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');
setError("You cannot rent your own item");
}
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to fetch item');
setError(err.response?.data?.message || "Failed to fetch item");
} finally {
setLoading(false);
}
};
const calculateTotal = () => {
if (!item || selectedPeriods.length === 0) {
setTotalCost(0);
setRentalDuration({ days: 0, hours: 0 });
return;
}
// For now, we'll use the first selected period
const period = selectedPeriods[0];
const start = new Date(period.startDate);
const end = new Date(period.endDate);
// Add time if hourly rental
if (item.pricePerHour && period.startTime && period.endTime) {
const [startHour, startMin] = period.startTime.split(':').map(Number);
const [endHour, endMin] = period.endTime.split(':').map(Number);
start.setHours(startHour, startMin);
end.setHours(endHour, endMin);
}
const diffMs = end.getTime() - start.getTime();
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
let cost = 0;
let duration = { days: 0, hours: 0 };
if (item.pricePerHour && period.startTime && period.endTime) {
// Hourly rental
cost = diffHours * item.pricePerHour;
duration.hours = diffHours;
} else if (item.pricePerDay) {
// Daily rental
cost = diffDays * item.pricePerDay;
duration.days = diffDays;
}
setTotalCost(cost);
setRentalDuration(duration);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user || !item) return;
@@ -161,121 +114,70 @@ const RentItem: React.FC = () => {
setError(null);
try {
if (selectedPeriods.length === 0) {
setError('Please select a rental period');
if (!manualSelection.startDate || !manualSelection.endDate) {
setError("Please select a rental period");
setSubmitting(false);
return;
}
const period = selectedPeriods[0];
const rentalData = {
itemId: item.id,
startDate: period.startDate.toISOString().split('T')[0],
endDate: period.endDate.toISOString().split('T')[0],
startTime: period.startTime || undefined,
endTime: period.endTime || undefined,
startDate: manualSelection.startDate,
endDate: manualSelection.endDate,
startTime: manualSelection.startTime,
endTime: manualSelection.endTime,
totalAmount: totalCost,
deliveryMethod: formData.deliveryMethod,
deliveryAddress: formData.deliveryMethod === 'delivery' ? formData.deliveryAddress : undefined
deliveryMethod: "pickup",
};
await rentalAPI.createRental(rentalData);
navigate('/my-rentals');
navigate("/my-rentals");
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to create rental');
setError(err.response?.data?.message || "Failed to create rental");
setSubmitting(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
const { name, value } = e.target;
if (name === 'cardNumber') {
if (name === "cardNumber") {
// Remove all non-digits
const cleaned = value.replace(/\D/g, '');
const cleaned = value.replace(/\D/g, "");
// Add spaces every 4 digits
const formatted = cleaned.match(/.{1,4}/g)?.join(' ') || cleaned;
const formatted = cleaned.match(/.{1,4}/g)?.join(" ") || cleaned;
// Limit to 16 digits (19 characters with spaces)
if (cleaned.length <= 16) {
setFormData(prev => ({ ...prev, [name]: formatted }));
setFormData((prev) => ({ ...prev, [name]: formatted }));
}
} else if (name === 'cardExpiry') {
} else if (name === "cardExpiry") {
// Remove all non-digits
const cleaned = value.replace(/\D/g, '');
const cleaned = value.replace(/\D/g, "");
// Add slash after 2 digits
let formatted = cleaned;
if (cleaned.length >= 3) {
formatted = cleaned.slice(0, 2) + '/' + cleaned.slice(2, 4);
formatted = cleaned.slice(0, 2) + "/" + cleaned.slice(2, 4);
}
// Limit to 4 digits
if (cleaned.length <= 4) {
setFormData(prev => ({ ...prev, [name]: formatted }));
setFormData((prev) => ({ ...prev, [name]: formatted }));
}
} else if (name === 'cardCVC') {
} else if (name === "cardCVC") {
// Only allow digits and limit to 4
const cleaned = value.replace(/\D/g, '');
const cleaned = value.replace(/\D/g, "");
if (cleaned.length <= 4) {
setFormData(prev => ({ ...prev, [name]: cleaned }));
setFormData((prev) => ({ ...prev, [name]: cleaned }));
}
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleManualSelectionChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const updatedSelection = {
...manualSelection,
[e.target.name]: e.target.value
};
setManualSelection(updatedSelection);
// Automatically apply selection if both dates are set
if (updatedSelection.startDate && updatedSelection.endDate) {
// Create dates with time set to midnight to avoid timezone issues
const start = new Date(updatedSelection.startDate + 'T00:00:00');
const end = new Date(updatedSelection.endDate + 'T00:00:00');
// Add time for both hourly and daily rentals
const [startHour, startMin] = updatedSelection.startTime.split(':').map(Number);
const [endHour, endMin] = updatedSelection.endTime.split(':').map(Number);
start.setHours(startHour, startMin, 0, 0);
end.setHours(endHour, endMin, 0, 0);
// Note: We keep the times as selected by the user
// The calendar will interpret 00:00 correctly
// Validate dates
if (end < start) {
setError('End date/time must be after start date/time');
return;
}
// Check if dates are available
const unavailable = item?.unavailablePeriods?.some(period => {
const periodStart = new Date(period.startDate);
const periodEnd = new Date(period.endDate);
return (start >= periodStart && start <= periodEnd) ||
(end >= periodStart && end <= periodEnd) ||
(start <= periodStart && end >= periodEnd);
});
if (unavailable) {
setError('Selected dates include unavailable periods');
return;
}
setError(null);
setSelectedPeriods([{
id: Date.now().toString(),
startDate: start,
endDate: end,
startTime: updatedSelection.startTime,
endTime: updatedSelection.endTime
}]);
setFormData((prev) => ({ ...prev, [name]: value }));
}
};
@@ -291,11 +193,15 @@ const RentItem: React.FC = () => {
);
}
if (!item || error === 'You cannot rent your own item' || error === 'This item is not available for rent') {
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'}
{error || "Item not found"}
</div>
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
Go Back
@@ -304,14 +210,11 @@ const RentItem: React.FC = () => {
);
}
const showHourlyOptions = !!item.pricePerHour;
const minDays = item.minimumRentalDays || 1;
return (
<div className="container mt-4">
<div className="row">
<div className="col-md-8">
<h1>Rent: {item.name}</h1>
<h1>Renting {item.name}</h1>
{error && (
<div className="alert alert-danger" role="alert">
@@ -319,158 +222,9 @@ const RentItem: React.FC = () => {
</div>
)}
<div className="row">
<div className="col-md-8">
<form onSubmit={handleSubmit}>
<div className="card mb-4">
<div className="card-body">
<h5 className="card-title">Select Rental Period</h5>
<AvailabilityCalendar
unavailablePeriods={item.unavailablePeriods || []}
onPeriodsChange={() => {}} // Read-only for renters
mode="renter"
selectedRentalPeriod={selectedPeriods.length > 0 ? {
id: selectedPeriods[0].id,
startDate: selectedPeriods[0].startDate,
endDate: selectedPeriods[0].endDate,
startTime: selectedPeriods[0].startTime,
endTime: selectedPeriods[0].endTime,
isRentalSelection: true
} : undefined}
onRentalSelect={(period) => {
// Update selected periods
setSelectedPeriods([{
id: Date.now().toString(),
startDate: period.startDate,
endDate: period.endDate,
startTime: period.startTime,
endTime: period.endTime
}]);
// Update manual selection to match calendar
setManualSelection({
startDate: period.startDate.toISOString().split('T')[0],
startTime: period.startTime || '09:00',
endDate: period.endDate.toISOString().split('T')[0],
endTime: period.endTime || '17:00'
});
}}
/>
<div className="mt-4">
<div className="row g-3">
<div className="col-md-3">
<label htmlFor="startDate" className="form-label">Start Date</label>
<input
type="date"
className="form-control"
id="startDate"
name="startDate"
value={manualSelection.startDate}
onChange={handleManualSelectionChange}
min={new Date().toISOString().split('T')[0]}
/>
</div>
<div className="col-md-3">
<label htmlFor="startTime" className="form-label">Start Time</label>
<select
className="form-select"
id="startTime"
name="startTime"
value={manualSelection.startTime}
onChange={handleManualSelectionChange}
>
{Array.from({ length: 24 }, (_, i) => {
const hour = i === 0 ? 12 : i > 12 ? i - 12 : i;
const period = i < 12 ? 'AM' : 'PM';
return (
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
{hour}:00 {period}
</option>
);
})}
</select>
</div>
<div className="col-md-3">
<label htmlFor="endDate" className="form-label">End Date</label>
<input
type="date"
className="form-control"
id="endDate"
name="endDate"
value={manualSelection.endDate}
onChange={handleManualSelectionChange}
min={manualSelection.startDate || new Date().toISOString().split('T')[0]}
/>
</div>
<div className="col-md-3">
<label htmlFor="endTime" className="form-label">End Time</label>
<select
className="form-select"
id="endTime"
name="endTime"
value={manualSelection.endTime}
onChange={handleManualSelectionChange}
>
{Array.from({ length: 24 }, (_, i) => {
const hour = i === 0 ? 12 : i > 12 ? i - 12 : i;
const period = i < 12 ? 'AM' : 'PM';
return (
<option key={i} value={`${i.toString().padStart(2, '0')}:00`}>
{hour}:00 {period}
</option>
);
})}
</select>
</div>
</div>
</div>
{rentalDuration.days < minDays && rentalDuration.days > 0 && !showHourlyOptions && (
<div className="alert alert-warning mt-3">
Minimum rental period is {minDays} days
</div>
)}
</div>
</div>
<div className="card mb-4">
<div className="card-body">
<div className="my-3">
<select
className="form-select"
id="deliveryMethod"
name="deliveryMethod"
value={formData.deliveryMethod}
onChange={handleChange}
>
{item.pickUpAvailable && <option value="pickup">Pick-up</option>}
{item.localDeliveryAvailable && <option value="delivery">Delivery</option>}
</select>
</div>
{formData.deliveryMethod === 'delivery' && (
<div className="mb-3">
<label htmlFor="deliveryAddress" className="form-label">Delivery Address *</label>
<input
type="text"
className="form-control"
id="deliveryAddress"
name="deliveryAddress"
value={formData.deliveryAddress}
onChange={handleChange}
placeholder="Enter delivery address"
required={formData.deliveryMethod === 'delivery'}
/>
{item.localDeliveryRadius && (
<div className="form-text">
Delivery available within {item.localDeliveryRadius} miles
</div>
)}
</div>
)}
</div>
</div>
<div className="card mb-4">
<div className="card-body">
<h5 className="card-title">Payment</h5>
@@ -487,7 +241,10 @@ const RentItem: React.FC = () => {
checked
readOnly
/>
<label className="form-check-label" htmlFor="creditCard">
<label
className="form-check-label"
htmlFor="creditCard"
>
Credit/Debit Card
</label>
</div>
@@ -495,7 +252,9 @@ const RentItem: React.FC = () => {
<div className="row mb-3">
<div className="col-12">
<label htmlFor="cardNumber" className="form-label">Card Number *</label>
<label htmlFor="cardNumber" className="form-label">
Card Number *
</label>
<input
type="text"
className="form-control"
@@ -511,7 +270,9 @@ const RentItem: React.FC = () => {
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="cardExpiry" className="form-label">Expiry Date *</label>
<label htmlFor="cardExpiry" className="form-label">
Expiry Date *
</label>
<input
type="text"
className="form-control"
@@ -524,7 +285,9 @@ const RentItem: React.FC = () => {
/>
</div>
<div className="col-md-6">
<label htmlFor="cardCVC" className="form-label">CVC *</label>
<label htmlFor="cardCVC" className="form-label">
CVC *
</label>
<input
type="text"
className="form-control"
@@ -539,7 +302,9 @@ const RentItem: React.FC = () => {
</div>
<div className="mb-3">
<label htmlFor="cardName" className="form-label">Name on Card *</label>
<label htmlFor="cardName" className="form-label">
Name on Card *
</label>
<input
type="text"
className="form-control"
@@ -553,18 +318,22 @@ const RentItem: React.FC = () => {
</div>
<div className="alert alert-info small">
<i className="bi bi-info-circle"></i> Your payment information is secure and encrypted. You will only be charged after the owner accepts your rental request.
</div>
</div>
<i className="bi bi-info-circle"></i> Your payment
information is secure and encrypted. You will only be
charged after the owner accepts your rental request.
</div>
<div className="d-grid gap-2">
<button
type="submit"
className="btn btn-primary"
disabled={submitting || selectedPeriods.length === 0 || totalCost === 0 || (rentalDuration.days < minDays && !showHourlyOptions)}
disabled={
!manualSelection.startDate || !manualSelection.endDate
}
>
{submitting ? 'Processing...' : `Confirm Rental - $${totalCost.toFixed(2)}`}
{submitting
? "Processing..."
: `Confirm Rental - $${totalCost}`}
</button>
<button
type="button"
@@ -574,65 +343,75 @@ const RentItem: React.FC = () => {
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
<div className="col-md-4">
<div className="card">
<div className="card-body">
<h5 className="card-title">Rental Summary</h5>
{item.images && item.images[0] && (
<img
src={item.images[0]}
alt={item.name}
className="img-fluid rounded mb-3"
style={{ width: '100%', height: '200px', objectFit: 'cover' }}
style={{
width: "100%",
height: "150px",
objectFit: "cover",
}}
/>
)}
<h6>{item.name}</h6>
<p className="text-muted small">{item.location}</p>
<p className="text-muted small">
{item.city && item.state
? `${item.city}, ${item.state}`
: item.location}
</p>
<hr />
<div className="mb-3">
<strong>Pricing:</strong>
{item.pricePerHour && (
<div>${item.pricePerHour}/hour</div>
)}
{item.pricePerDay && (
<div>${item.pricePerDay}/day</div>
)}
</div>
{rentalDuration.days > 0 || rentalDuration.hours > 0 ? (
<>
<div className="mb-3">
<strong>Duration:</strong>
<div>
{rentalDuration.days > 0 && `${rentalDuration.days} days`}
{rentalDuration.hours > 0 && `${rentalDuration.hours} hours`}
</div>
</div>
<hr />
<div className="d-flex justify-content-between">
<strong>Total Cost:</strong>
<strong>${totalCost.toFixed(2)}</strong>
</div>
</>
{/* Pricing */}
<div className="mb-3 text-center">
{totalCost === 0 ? (
<h6>Free to Borrow</h6>
) : (
<p className="text-muted">Select dates to see total cost</p>
<>
{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>
)}
{item.rules && (
{/* Total Cost */}
{totalCost > 0 && (
<>
<hr />
<div>
<strong>Rules:</strong>
<p className="small">{item.rules}</p>
<div className="d-flex justify-content-between">
<strong>Total:</strong>
<strong>${totalCost}</strong>
</div>
</>
)}
@@ -641,6 +420,8 @@ const RentItem: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -69,6 +69,7 @@ export const itemAPI = {
updateItem: (id: string, data: any) => api.put(`/items/${id}`, data),
deleteItem: (id: string) => api.delete(`/items/${id}`),
getRecommendations: () => api.get("/items/recommendations"),
getItemReviews: (id: string) => api.get(`/items/${id}/reviews`),
};
export const rentalAPI = {