made components that create and edit item can share, started item detail changes, listings provide more views

This commit is contained in:
jackiettran
2025-08-20 17:06:47 -04:00
parent ddd27a59f9
commit b624841350
13 changed files with 1008 additions and 982 deletions

View File

@@ -0,0 +1,60 @@
import React from 'react';
interface DeliveryOptionsProps {
pickUpAvailable: boolean;
inPlaceUseAvailable: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const DeliveryOptions: React.FC<DeliveryOptionsProps> = ({
pickUpAvailable,
inPlaceUseAvailable,
onChange
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<label className="form-label">
How will renters receive this item?
</label>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="pickUpAvailable"
name="pickUpAvailable"
checked={pickUpAvailable}
onChange={onChange}
/>
<label className="form-check-label" htmlFor="pickUpAvailable">
Pick-Up/Drop-off
<div className="small text-muted">
You and the renter coordinate pick-up and drop-off
</div>
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="inPlaceUseAvailable"
name="inPlaceUseAvailable"
checked={inPlaceUseAvailable}
onChange={onChange}
/>
<label
className="form-check-label"
htmlFor="inPlaceUseAvailable"
>
In-Place Use
<div className="small text-muted">
They use at your location
</div>
</label>
</div>
</div>
</div>
);
};
export default DeliveryOptions;

View File

@@ -0,0 +1,70 @@
import React from 'react';
interface ImageUploadProps {
imageFiles: File[];
imagePreviews: string[];
onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onRemoveImage: (index: number) => void;
error: string;
}
const ImageUpload: React.FC<ImageUploadProps> = ({
imageFiles,
imagePreviews,
onImageChange,
onRemoveImage,
error
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label className="form-label mb-0">
Upload Images (Max 5)
</label>
<div className="form-text mb-2">
Have pictures of everything that's included
</div>
<input
type="file"
className="form-control"
onChange={onImageChange}
accept="image/*"
multiple
disabled={imageFiles.length >= 5}
/>
</div>
{imagePreviews.length > 0 && (
<div className="row mt-3">
{imagePreviews.map((preview, index) => (
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
<div className="position-relative">
<img
src={preview}
alt={`Preview ${index + 1}`}
className="img-fluid rounded"
style={{
width: "100%",
height: "150px",
objectFit: "cover",
}}
/>
<button
type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
onClick={() => onRemoveImage(index)}
>
<i className="bi bi-x"></i>
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default ImageUpload;

View File

@@ -0,0 +1,53 @@
import React from "react";
interface ItemInformationProps {
name: string;
description: string;
onChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
}
const ItemInformation: React.FC<ItemInformationProps> = ({
name,
description,
onChange,
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label htmlFor="name" className="form-label">
Item Name *
</label>
<input
type="text"
className="form-control"
id="name"
name="name"
value={name}
onChange={onChange}
required
/>
</div>
<div className="mb-3">
<label htmlFor="description" className="form-label">
Description *
</label>
<textarea
className="form-control"
id="description"
name="description"
rows={4}
value={description}
onChange={onChange}
required
/>
</div>
</div>
</div>
);
};
export default ItemInformation;

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Rental } from '../types';
import { rentalAPI } from '../services/api';
import React, { useState, useEffect } from "react";
import { Rental } from "../types";
import { rentalAPI } from "../services/api";
interface ItemReviewsProps {
itemId: string;
@@ -20,24 +20,25 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
// Fetch all rentals for this item
const response = await rentalAPI.getMyListings();
const allRentals: Rental[] = 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
(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);
}
} catch (error) {
console.error('Failed to fetch reviews:', error);
console.error("Failed to fetch reviews:", error);
} finally {
setLoading(false);
}
@@ -49,7 +50,7 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
{[1, 2, 3, 4, 5].map((star) => (
<i
key={star}
className={`bi ${star <= rating ? 'bi-star-fill' : 'bi-star'}`}
className={`bi ${star <= rating ? "bi-star-fill" : "bi-star"}`}
></i>
))}
</span>
@@ -72,16 +73,18 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
return (
<div className="mb-4">
<h5>Reviews</h5>
{reviews.length === 0 ? (
<p className="text-muted">No reviews yet. Be the first to rent and review this item!</p>
<p className="text-muted">Be the first to rent and review this item!</p>
) : (
<>
<div className="mb-3">
<div className="d-flex align-items-center gap-2">
{renderStars(Math.round(averageRating))}
<span className="fw-bold">{averageRating.toFixed(1)}</span>
<span className="text-muted">({reviews.length} {reviews.length === 1 ? 'review' : 'reviews'})</span>
<span className="text-muted">
({reviews.length} {reviews.length === 1 ? "review" : "reviews"})
</span>
</div>
</div>
@@ -96,18 +99,27 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
src={rental.renter.profileImage}
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
className="rounded-circle"
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
style={{
width: "32px",
height: "32px",
objectFit: "cover",
}}
/>
) : (
<div
<div
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
style={{ width: '32px', height: '32px' }}
style={{ width: "32px", height: "32px" }}
>
<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>
)}
<div>
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong>
<strong>
{rental.renter?.firstName} {rental.renter?.lastName}
</strong>
<div className="small">
{renderStars(rental.rating || 0)}
</div>
@@ -128,4 +140,4 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
);
};
export default ItemReviews;
export default ItemReviews;

View File

@@ -0,0 +1,179 @@
import React from 'react';
import { Address } from '../types';
interface LocationFormData {
address1: string;
address2: string;
city: string;
state: string;
zipCode: string;
country: string;
latitude?: number;
longitude?: number;
}
interface LocationFormProps {
data: LocationFormData;
userAddresses: Address[];
selectedAddressId: string;
addressesLoading: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
onAddressSelect: (addressId: string) => void;
formatAddressDisplay: (address: Address) => string;
}
const LocationForm: React.FC<LocationFormProps> = ({
data,
userAddresses,
selectedAddressId,
addressesLoading,
onChange,
onAddressSelect,
formatAddressDisplay
}) => {
const usStates = [
"Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut",
"Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa",
"Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan",
"Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire",
"New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio",
"Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota",
"Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia",
"Wisconsin", "Wyoming"
];
return (
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<small className="text-muted">
<i className="bi bi-info-circle me-2"></i>
Your address is private. This will only be used to show
renters a general area.
</small>
</div>
{addressesLoading ? (
<div className="text-center py-3">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading addresses...</span>
</div>
</div>
) : (
<>
{/* Multiple addresses - show dropdown */}
{userAddresses.length > 1 && (
<div className="mb-3">
<label className="form-label">Select Address</label>
<select
className="form-select"
value={selectedAddressId || "new"}
onChange={(e) => onAddressSelect(e.target.value)}
>
<option value="new">Enter new address</option>
{userAddresses.map(address => (
<option key={address.id} value={address.id}>
{formatAddressDisplay(address)}
</option>
))}
</select>
</div>
)}
{/* Show form fields for all scenarios with addresses <= 1 or when "new" is selected */}
{(userAddresses.length <= 1 ||
(userAddresses.length > 1 && !selectedAddressId)) && (
<>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address1" className="form-label">
Address Line 1 *
</label>
<input
type="text"
className="form-control"
id="address1"
name="address1"
value={data.address1}
onChange={onChange}
placeholder="123 Main Street"
required
/>
</div>
<div className="col-md-6">
<label htmlFor="address2" className="form-label">
Address Line 2
</label>
<input
type="text"
className="form-control"
id="address2"
name="address2"
value={data.address2}
onChange={onChange}
placeholder="Apt, Suite, Unit, etc."
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="city" className="form-label">
City *
</label>
<input
type="text"
className="form-control"
id="city"
name="city"
value={data.city}
onChange={onChange}
required
/>
</div>
<div className="col-md-3">
<label htmlFor="state" className="form-label">
State *
</label>
<select
className="form-select"
id="state"
name="state"
value={data.state}
onChange={onChange}
required
>
<option value="">Select State</option>
{usStates.map(state => (
<option key={state} value={state}>
{state}
</option>
))}
</select>
</div>
<div className="col-md-3">
<label htmlFor="zipCode" className="form-label">
ZIP Code *
</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={data.zipCode}
onChange={onChange}
placeholder="12345"
required
/>
</div>
</div>
</>
)}
</>
)}
</div>
</div>
);
};
export default LocationForm;

View File

@@ -0,0 +1,102 @@
import React from 'react';
interface PricingFormProps {
priceType: "hour" | "day";
pricePerHour: number | string;
pricePerDay: number | string;
replacementCost: number | string;
minimumRentalDays: number;
onPriceTypeChange: (type: "hour" | "day") => void;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
}
const PricingForm: React.FC<PricingFormProps> = ({
priceType,
pricePerHour,
pricePerDay,
replacementCost,
minimumRentalDays,
onPriceTypeChange,
onChange
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<div className="row align-items-center">
<div className="col-auto">
<label className="col-form-label">Price per</label>
</div>
<div className="col-auto">
<select
className="form-select"
value={priceType}
onChange={(e) => onPriceTypeChange(e.target.value as "hour" | "day")}
>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div className="col">
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
value={priceType === "hour" ? pricePerHour : pricePerDay}
onChange={onChange}
step="0.01"
min="0"
placeholder="0.00"
/>
</div>
</div>
</div>
</div>
<div className="mb-3">
<label htmlFor="minimumRentalDays" className="form-label">
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
</label>
<input
type="number"
className="form-control"
id="minimumRentalDays"
name="minimumRentalDays"
value={minimumRentalDays}
onChange={onChange}
min="1"
/>
</div>
<div className="mb-3">
<label htmlFor="replacementCost" className="form-label mb-0">
Replacement Cost *
</label>
<div className="form-text mb-2">
The cost to replace the item if lost
</div>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="replacementCost"
name="replacementCost"
value={replacementCost}
onChange={onChange}
step="0.01"
min="0"
placeholder="0"
required
/>
</div>
</div>
</div>
</div>
);
};
export default PricingForm;

View File

@@ -0,0 +1,47 @@
import React from 'react';
interface RulesFormProps {
needsTraining: boolean;
rules: string;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
}
const RulesForm: React.FC<RulesFormProps> = ({
needsTraining,
rules,
onChange
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<div className="form-check mb-3">
<input
type="checkbox"
className="form-check-input"
id="needsTraining"
name="needsTraining"
checked={needsTraining}
onChange={onChange}
/>
<label className="form-check-label" htmlFor="needsTraining">
Requires in-person training before rental
</label>
</div>
<label htmlFor="rules" className="form-label">
Additional Rules
</label>
<textarea
className="form-control"
id="rules"
name="rules"
rows={3}
value={rules || ""}
onChange={onChange}
placeholder="Any specific rules for renting this item"
/>
</div>
</div>
);
};
export default RulesForm;

View File

@@ -3,6 +3,12 @@ import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ImageUpload from "../components/ImageUpload";
import ItemInformation from "../components/ItemInformation";
import LocationForm from "../components/LocationForm";
import DeliveryOptions from "../components/DeliveryOptions";
import PricingForm from "../components/PricingForm";
import RulesForm from "../components/RulesForm";
import { Address } from "../types";
interface ItemFormData {
@@ -89,15 +95,15 @@ const CreateItem: React.FC = () => {
try {
const response = await userAPI.getAvailability();
const userAvailability = response.data;
setFormData(prev => ({
setFormData((prev) => ({
...prev,
generalAvailableAfter: userAvailability.generalAvailableAfter,
generalAvailableBefore: userAvailability.generalAvailableBefore,
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
weeklyTimes: userAvailability.weeklyTimes
weeklyTimes: userAvailability.weeklyTimes,
}));
} catch (error) {
console.error('Error fetching user availability:', error);
console.error("Error fetching user availability:", error);
// Use default values if fetch fails
}
};
@@ -165,9 +171,19 @@ const CreateItem: React.FC = () => {
const response = await api.post("/items", {
...formData,
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
pricePerDay: formData.pricePerDay
? parseFloat(formData.pricePerDay.toString())
: undefined,
pricePerHour: formData.pricePerHour
? parseFloat(formData.pricePerHour.toString())
: undefined,
replacementCost: formData.replacementCost
? parseFloat(formData.replacementCost.toString())
: 0,
availableAfter: formData.generalAvailableAfter,
availableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
location,
images: imageUrls,
});
@@ -196,14 +212,14 @@ const CreateItem: React.FC = () => {
try {
const userItemsResponse = await itemAPI.getItems({ owner: user.id });
const userItems = userItemsResponse.data.items || [];
// If this is their first item (the one we just created), save availability to user
if (userItems.length <= 1) {
await userAPI.updateAvailability({
generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes
weeklyTimes: formData.weeklyTimes,
});
}
} catch (availabilityError) {
@@ -277,59 +293,6 @@ const CreateItem: React.FC = () => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
const usStates = [
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming",
];
const handleWeeklyTimeChange = (
day: string,
field: "availableAfter" | "availableBefore",
@@ -387,271 +350,44 @@ const CreateItem: React.FC = () => {
)}
<form onSubmit={handleSubmit}>
{/* Images Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label className="form-label mb-0">
Upload Images (Max 5)
</label>
<div className="form-text mb-2">
Have pictures of everything that's included
</div>
<input
type="file"
className="form-control"
onChange={handleImageChange}
accept="image/*"
multiple
disabled={imageFiles.length >= 5}
/>
</div>
<ImageUpload
imageFiles={imageFiles}
imagePreviews={imagePreviews}
onImageChange={handleImageChange}
onRemoveImage={removeImage}
error={error}
/>
{imagePreviews.length > 0 && (
<div className="row mt-3">
{imagePreviews.map((preview, index) => (
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
<div className="position-relative">
<img
src={preview}
alt={`Preview ${index + 1}`}
className="img-fluid rounded"
style={{
width: "100%",
height: "150px",
objectFit: "cover",
}}
/>
<button
type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
onClick={() => removeImage(index)}
>
<i className="bi bi-x"></i>
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
<ItemInformation
name={formData.name}
description={formData.description}
onChange={handleChange}
/>
{/* Basic Information Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label htmlFor="name" className="form-label">
Item Name *
</label>
<input
type="text"
className="form-control"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<LocationForm
data={{
address1: formData.address1,
address2: formData.address2,
city: formData.city,
state: formData.state,
zipCode: formData.zipCode,
country: formData.country,
latitude: formData.latitude,
longitude: formData.longitude,
}}
userAddresses={userAddresses}
selectedAddressId={selectedAddressId}
addressesLoading={addressesLoading}
onChange={handleChange}
onAddressSelect={handleAddressSelect}
formatAddressDisplay={formatAddressDisplay}
/>
<div className="mb-3">
<label htmlFor="description" className="form-label">
Description *
</label>
<textarea
className="form-control"
id="description"
name="description"
rows={4}
value={formData.description}
onChange={handleChange}
required
/>
</div>
</div>
</div>
{/* Location Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<small className="text-muted">
<i className="bi bi-info-circle me-2"></i>
This address is private. It will only be used to show
renters a general area.
</small>
</div>
{addressesLoading ? (
<div className="text-center py-3">
<div
className="spinner-border spinner-border-sm"
role="status"
>
<span className="visually-hidden">
Loading addresses...
</span>
</div>
</div>
) : (
<>
{/* Multiple addresses - show dropdown */}
{userAddresses.length > 1 && (
<div className="mb-3">
<label className="form-label">Select Address</label>
<select
className="form-select"
value={selectedAddressId || "new"}
onChange={(e) => handleAddressSelect(e.target.value)}
>
<option value="new">Enter new address</option>
{userAddresses.map((address) => (
<option key={address.id} value={address.id}>
{formatAddressDisplay(address)}
</option>
))}
</select>
</div>
)}
{/* Show form fields for all scenarios */}
{(userAddresses.length <= 1 ||
(userAddresses.length > 1 && !selectedAddressId)) && (
<>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address1" className="form-label">
Address Line 1 *
</label>
<input
type="text"
className="form-control"
id="address1"
name="address1"
value={formData.address1}
onChange={handleChange}
placeholder=""
required
/>
</div>
<div className="col-md-6">
<label htmlFor="address2" className="form-label">
Address Line 2
</label>
<input
type="text"
className="form-control"
id="address2"
name="address2"
value={formData.address2}
onChange={handleChange}
placeholder="Apt, Suite, Unit, etc."
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="city" className="form-label">
City *
</label>
<input
type="text"
className="form-control"
id="city"
name="city"
value={formData.city}
onChange={handleChange}
required
/>
</div>
<div className="col-md-3">
<label htmlFor="state" className="form-label">
State *
</label>
<select
className="form-select"
id="state"
name="state"
value={formData.state}
onChange={handleChange}
required
>
<option value="">Select State</option>
{usStates.map((state) => (
<option key={state} value={state}>
{state}
</option>
))}
</select>
</div>
<div className="col-md-3">
<label htmlFor="zipCode" className="form-label">
ZIP Code *
</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder=""
required
/>
</div>
</div>
</>
)}
</>
)}
</div>
</div>
{/* Delivery & Availability Card */}
<div className="card mb-4">
<div className="card-body">
<label className="form-label">
How will renters receive this item?
</label>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="pickUpAvailable"
name="pickUpAvailable"
checked={formData.pickUpAvailable}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="pickUpAvailable">
Pick-Up/Drop-off
<div className="small text-muted">
You and the renter coordinate pick-up and drop-off
</div>
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="inPlaceUseAvailable"
name="inPlaceUseAvailable"
checked={formData.inPlaceUseAvailable}
onChange={handleChange}
/>
<label
className="form-check-label"
htmlFor="inPlaceUseAvailable"
>
In-Place Use
<div className="small text-muted">
They use at your location
</div>
</label>
</div>
</div>
</div>
<DeliveryOptions
pickUpAvailable={formData.pickUpAvailable}
inPlaceUseAvailable={formData.inPlaceUseAvailable}
onChange={handleChange}
/>
{/* Availability Card */}
<div className="card mb-4">
@@ -671,128 +407,21 @@ const CreateItem: React.FC = () => {
</div>
</div>
{/* Pricing Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<div className="row align-items-center">
<div className="col-auto">
<label className="col-form-label">Price per</label>
</div>
<div className="col-auto">
<select
className="form-select"
value={priceType}
onChange={(e) =>
setPriceType(e.target.value as "hour" | "day")
}
>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div className="col">
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id={
priceType === "hour"
? "pricePerHour"
: "pricePerDay"
}
name={
priceType === "hour"
? "pricePerHour"
: "pricePerDay"
}
value={
priceType === "hour"
? formData.pricePerHour || ""
: formData.pricePerDay || ""
}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
/>
</div>
</div>
</div>
</div>
<PricingForm
priceType={priceType}
pricePerHour={formData.pricePerHour || ""}
pricePerDay={formData.pricePerDay || ""}
replacementCost={formData.replacementCost}
minimumRentalDays={formData.minimumRentalDays}
onPriceTypeChange={setPriceType}
onChange={handleChange}
/>
<div className="mb-3">
<label htmlFor="minimumRentalDays" className="form-label">
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
</label>
<input
type="number"
className="form-control"
id="minimumRentalDays"
name="minimumRentalDays"
value={formData.minimumRentalDays}
onChange={handleChange}
min="1"
/>
</div>
<div className="mb-3">
<label htmlFor="replacementCost" className="form-label mb-0">
Replacement Cost *
</label>
<div className="form-text mb-2">
The cost to replace the item if lost
</div>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="replacementCost"
name="replacementCost"
value={formData.replacementCost}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0"
required
/>
</div>
</div>
</div>
</div>
{/* Rules & Guidelines Card */}
<div className="card mb-4">
<div className="card-body">
<div className="form-check mb-3">
<input
type="checkbox"
className="form-check-input"
id="needsTraining"
name="needsTraining"
checked={formData.needsTraining}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="needsTraining">
Requires in-person training before rental
</label>
</div>
<label htmlFor="rules" className="form-label">
Additional Rules
</label>
<textarea
className="form-control"
id="rules"
name="rules"
rows={3}
value={formData.rules || ""}
onChange={handleChange}
placeholder="Any specific rules for renting this item"
/>
</div>
</div>
<RulesForm
needsTraining={formData.needsTraining}
rules={formData.rules || ""}
onChange={handleChange}
/>
<div className="d-grid gap-2 mb-5">
<button

View File

@@ -1,36 +1,47 @@
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Item, Rental } from "../types";
import { Item, Rental, Address } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI } from "../services/api";
import AvailabilityCalendar from "../components/AvailabilityCalendar";
import AddressAutocomplete from "../components/AddressAutocomplete";
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ImageUpload from "../components/ImageUpload";
import ItemInformation from "../components/ItemInformation";
import LocationForm from "../components/LocationForm";
import DeliveryOptions from "../components/DeliveryOptions";
import PricingForm from "../components/PricingForm";
import RulesForm from "../components/RulesForm";
interface ItemFormData {
name: string;
description: string;
pickUpAvailable: boolean;
localDeliveryAvailable: boolean;
localDeliveryRadius?: number;
shippingAvailable: boolean;
inPlaceUseAvailable: boolean;
pricePerHour?: number | string;
pricePerDay?: number | string;
replacementCost: number | string;
location: string;
address1: string;
address2: string;
city: string;
state: string;
zipCode: string;
country: string;
latitude?: number;
longitude?: number;
rules?: string;
minimumRentalDays: number;
needsTraining: boolean;
availability: boolean;
unavailablePeriods?: Array<{
id: string;
startDate: Date;
endDate: Date;
startTime?: string;
endTime?: string;
}>;
generalAvailableAfter: string;
generalAvailableBefore: string;
specifyTimesPerDay: boolean;
weeklyTimes: {
sunday: { availableAfter: string; availableBefore: string };
monday: { availableAfter: string; availableBefore: string };
tuesday: { availableAfter: string; availableBefore: string };
wednesday: { availableAfter: string; availableBefore: string };
thursday: { availableAfter: string; availableBefore: string };
friday: { availableAfter: string; availableBefore: string };
saturday: { availableAfter: string; availableBefore: string };
};
}
const EditItem: React.FC = () => {
@@ -44,29 +55,57 @@ const EditItem: React.FC = () => {
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
const [priceType, setPriceType] = useState<"hour" | "day">("day");
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
const [addressesLoading, setAddressesLoading] = useState(true);
const [formData, setFormData] = useState<ItemFormData>({
name: "",
description: "",
pickUpAvailable: false,
localDeliveryAvailable: false,
shippingAvailable: false,
inPlaceUseAvailable: false,
pricePerHour: undefined,
pricePerDay: undefined,
replacementCost: 0,
location: "",
pricePerHour: "",
pricePerDay: "",
replacementCost: "",
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
rules: "",
minimumRentalDays: 1,
needsTraining: false,
availability: true,
unavailablePeriods: [],
generalAvailableAfter: "09:00",
generalAvailableBefore: "17:00",
specifyTimesPerDay: false,
weeklyTimes: {
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
monday: { availableAfter: "09:00", availableBefore: "17:00" },
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
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" },
},
});
useEffect(() => {
fetchItem();
fetchAcceptedRentals();
fetchUserAddresses();
}, [id]);
const fetchUserAddresses = async () => {
try {
const response = await addressAPI.getAddresses();
setUserAddresses(response.data);
} catch (error) {
console.error("Error fetching addresses:", error);
} finally {
setAddressesLoading(false);
}
};
const fetchItem = async () => {
try {
const response = await itemAPI.getItem(id!);
@@ -89,21 +128,33 @@ const EditItem: React.FC = () => {
name: item.name,
description: item.description,
pickUpAvailable: item.pickUpAvailable || false,
localDeliveryAvailable: item.localDeliveryAvailable || false,
localDeliveryRadius: item.localDeliveryRadius || 25,
shippingAvailable: item.shippingAvailable || false,
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
pricePerHour: item.pricePerHour,
pricePerDay: item.pricePerDay,
replacementCost: item.replacementCost,
location: item.location,
pricePerHour: item.pricePerHour || "",
pricePerDay: item.pricePerDay || "",
replacementCost: item.replacementCost || "",
address1: item.address1 || "",
address2: item.address2 || "",
city: item.city || "",
state: item.state || "",
zipCode: item.zipCode || "",
country: item.country || "US",
latitude: item.latitude,
longitude: item.longitude,
rules: item.rules || "",
minimumRentalDays: item.minimumRentalDays,
needsTraining: item.needsTraining || false,
availability: item.availability,
unavailablePeriods: item.unavailablePeriods || [],
generalAvailableAfter: item.availableAfter || "09:00",
generalAvailableBefore: item.availableBefore || "17:00",
specifyTimesPerDay: item.specifyTimesPerDay || false,
weeklyTimes: item.weeklyTimes || {
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
monday: { availableAfter: "09:00", availableBefore: "17:00" },
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
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" },
},
});
// Set existing images as previews
@@ -163,12 +214,43 @@ const EditItem: React.FC = () => {
await itemAPI.updateItem(id!, {
...formData,
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
pricePerDay: formData.pricePerDay
? parseFloat(formData.pricePerDay.toString())
: undefined,
pricePerHour: formData.pricePerHour
? parseFloat(formData.pricePerHour.toString())
: undefined,
replacementCost: formData.replacementCost
? parseFloat(formData.replacementCost.toString())
: 0,
availableAfter: formData.generalAvailableAfter,
availableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
images: imageUrls,
});
// Check if user has other items - only save to user profile if no other items
try {
const userItemsResponse = await itemAPI.getItems({ owner: user?.id });
const userItems = userItemsResponse.data.items || [];
const hasOtherItems =
userItems.filter((item: Item) => item.id !== id).length > 0;
if (!hasOtherItems) {
// Save to user profile - this is still their primary/only item
await userAPI.updateAvailability({
generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
});
}
} catch (availabilityError) {
console.error("Failed to save availability:", availabilityError);
// Don't fail item update if availability save fails
}
setSuccess(true);
setTimeout(() => {
navigate(`/items/${id}`);
@@ -204,6 +286,60 @@ const EditItem: React.FC = () => {
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
};
const handleAddressSelect = (addressId: string) => {
if (addressId === "new") {
setFormData((prev) => ({
...prev,
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
}));
setSelectedAddressId("");
} else {
const selectedAddress = userAddresses.find(
(addr) => addr.id === addressId
);
if (selectedAddress) {
setFormData((prev) => ({
...prev,
address1: selectedAddress.address1,
address2: selectedAddress.address2 || "",
city: selectedAddress.city,
state: selectedAddress.state,
zipCode: selectedAddress.zipCode,
country: selectedAddress.country,
latitude: selectedAddress.latitude,
longitude: selectedAddress.longitude,
}));
setSelectedAddressId(addressId);
}
}
};
const formatAddressDisplay = (address: Address) => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
const handleWeeklyTimeChange = (
day: string,
field: "availableAfter" | "availableBefore",
value: string
) => {
setFormData((prev) => ({
...prev,
weeklyTimes: {
...prev.weeklyTimes,
[day]: {
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
[field]: value,
},
},
}));
};
if (loading) {
return (
<div className="container mt-5">
@@ -245,386 +381,79 @@ const EditItem: React.FC = () => {
)}
<form onSubmit={handleSubmit}>
{/* Images Card */}
<ImageUpload
imageFiles={imageFiles}
imagePreviews={imagePreviews}
onImageChange={handleImageChange}
onRemoveImage={removeImage}
error={error || ""}
/>
<ItemInformation
name={formData.name}
description={formData.description}
onChange={handleChange}
/>
<LocationForm
data={{
address1: formData.address1,
address2: formData.address2,
city: formData.city,
state: formData.state,
zipCode: formData.zipCode,
country: formData.country,
latitude: formData.latitude,
longitude: formData.longitude,
}}
userAddresses={userAddresses}
selectedAddressId={selectedAddressId}
addressesLoading={addressesLoading}
onChange={handleChange}
onAddressSelect={handleAddressSelect}
formatAddressDisplay={formatAddressDisplay}
/>
<DeliveryOptions
pickUpAvailable={formData.pickUpAvailable}
inPlaceUseAvailable={formData.inPlaceUseAvailable}
onChange={handleChange}
/>
<PricingForm
priceType={priceType}
pricePerHour={formData.pricePerHour || ""}
pricePerDay={formData.pricePerDay || ""}
replacementCost={formData.replacementCost}
minimumRentalDays={formData.minimumRentalDays}
onPriceTypeChange={setPriceType}
onChange={handleChange}
/>
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label className="form-label">Images (Max 5)</label>
<input
type="file"
className="form-control"
onChange={handleImageChange}
accept="image/*"
multiple
disabled={imagePreviews.length >= 5}
/>
<div className="form-text">
Have pictures of everything that's included
</div>
</div>
{imagePreviews.length > 0 && (
<div className="row mt-3">
{imagePreviews.map((preview, index) => (
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
<div className="position-relative">
<img
src={preview}
alt={`Preview ${index + 1}`}
className="img-fluid rounded"
style={{
width: "100%",
height: "150px",
objectFit: "cover",
}}
/>
<button
type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
onClick={() => removeImage(index)}
>
<i className="bi bi-x"></i>
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Basic Information Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label htmlFor="name" className="form-label">
Item Name *
</label>
<input
type="text"
className="form-control"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="mb-3">
<label htmlFor="description" className="form-label">
Description *
</label>
<textarea
className="form-control"
id="description"
name="description"
rows={4}
value={formData.description}
onChange={handleChange}
required
/>
</div>
</div>
</div>
{/* Location Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<label htmlFor="location" className="form-label">
Address *
</label>
<AddressAutocomplete
id="location"
name="location"
value={formData.location}
onChange={(value, lat, lon) => {
setFormData((prev) => ({
...prev,
location: value,
latitude: lat,
longitude: lon,
}));
}}
placeholder="Enter address"
required
/>
</div>
</div>
</div>
{/* Delivery & Availability Options Card */}
<div className="card mb-4">
<div className="card-body">
<label className="form-label">
How will renters receive this item?
</label>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="pickUpAvailable"
name="pickUpAvailable"
checked={formData.pickUpAvailable}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="pickUpAvailable">
Pick-Up/Drop-off
<div className="small text-muted">
You and the renter coordinate pick-up and drop-off
</div>
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="localDeliveryAvailable"
name="localDeliveryAvailable"
checked={formData.localDeliveryAvailable}
onChange={handleChange}
/>
<label
className="form-check-label d-flex align-items-center"
htmlFor="localDeliveryAvailable"
>
<div>
Local Delivery
{formData.localDeliveryAvailable && (
<span className="ms-2">
(Delivery Radius:
<input
type="number"
className="form-control form-control-sm d-inline-block mx-1"
id="localDeliveryRadius"
name="localDeliveryRadius"
value={formData.localDeliveryRadius || ""}
onChange={handleChange}
onClick={(e) => e.stopPropagation()}
placeholder="25"
min="1"
max="100"
style={{ width: "60px" }}
/>
miles)
</span>
)}
<div className="small text-muted">
You deliver and then pick-up the item when the rental
period ends
</div>
</div>
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="shippingAvailable"
name="shippingAvailable"
checked={formData.shippingAvailable}
onChange={handleChange}
/>
<label
className="form-check-label"
htmlFor="shippingAvailable"
>
Shipping
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="inPlaceUseAvailable"
name="inPlaceUseAvailable"
checked={formData.inPlaceUseAvailable}
onChange={handleChange}
/>
<label
className="form-check-label"
htmlFor="inPlaceUseAvailable"
>
In-Place Use
<div className="small text-muted">
They use at your location
</div>
</label>
</div>
</div>
</div>
{/* Pricing Card */}
<div className="card mb-4">
<div className="card-body">
<div className="mb-3">
<div className="row align-items-center">
<div className="col-auto">
<label className="col-form-label">Price per</label>
</div>
<div className="col-auto">
<select
className="form-select"
value={priceType}
onChange={(e) =>
setPriceType(e.target.value as "hour" | "day")
}
>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div className="col">
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id={
priceType === "hour"
? "pricePerHour"
: "pricePerDay"
}
name={
priceType === "hour"
? "pricePerHour"
: "pricePerDay"
}
value={
priceType === "hour"
? formData.pricePerHour || ""
: formData.pricePerDay || ""
}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
/>
</div>
</div>
</div>
</div>
<div className="mb-3">
<label htmlFor="minimumRentalDays" className="form-label">
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
</label>
<input
type="number"
className="form-control"
id="minimumRentalDays"
name="minimumRentalDays"
value={formData.minimumRentalDays}
onChange={handleChange}
min="1"
/>
</div>
<div className="mb-3">
<label htmlFor="replacementCost" className="form-label mb-0">
Replacement Cost *
</label>
<div className="form-text mb-2">
The cost to replace the item if lost
</div>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="replacementCost"
name="replacementCost"
value={formData.replacementCost}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0"
required
/>
</div>
</div>
</div>
</div>
{/* Availability Schedule Card */}
<div className="card mb-4">
<div className="card-body">
<p className="text-muted">
Select dates when the item is NOT available for rent. Dates
with accepted rentals are shown in purple.
</p>
<AvailabilityCalendar
unavailablePeriods={[
...(formData.unavailablePeriods || []),
...acceptedRentals.map((rental) => ({
id: `rental-${rental.id}`,
startDate: new Date(rental.startDate),
endDate: new Date(rental.endDate),
isAcceptedRental: true,
})),
]}
onPeriodsChange={(periods) => {
// Filter out accepted rental periods when saving
const userPeriods = periods.filter(
(p) => !p.isAcceptedRental
);
setFormData((prev) => ({
...prev,
unavailablePeriods: userPeriods,
}));
<AvailabilitySettings
data={{
generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
}}
mode="owner"
/>
<div className="mt-3 form-check">
<input
type="checkbox"
className="form-check-input"
id="availability"
name="availability"
checked={formData.availability}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="availability">
Available for rent
</label>
</div>
</div>
</div>
{/* Rules & Guidelines Card */}
<div className="card mb-4">
<div className="card-body">
<div className="form-check mb-3">
<input
type="checkbox"
className="form-check-input"
id="needsTraining"
name="needsTraining"
checked={formData.needsTraining}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="needsTraining">
Requires in-person training before rental
</label>
</div>
<label htmlFor="rules" className="form-label">
Additional Rules
</label>
<textarea
className="form-control"
id="rules"
name="rules"
rows={3}
value={formData.rules || ""}
onChange={handleChange}
placeholder="Any specific rules for renting this item"
onChange={(field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
}}
onWeeklyTimeChange={handleWeeklyTimeChange}
/>
</div>
</div>
<div className="d-grid gap-2">
<RulesForm
needsTraining={formData.needsTraining}
rules={formData.rules || ""}
onChange={handleChange}
/>
<div className="d-grid gap-2 mb-5">
<button
type="submit"
className="btn btn-primary"

View File

@@ -88,6 +88,15 @@ const ItemDetail: React.FC = () => {
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-10">
{isOwner && (
<div className="d-flex justify-content-end mb-3">
<button className="btn btn-outline-primary" onClick={handleEdit}>
<i className="bi bi-pencil me-2"></i>
Edit Listing
</button>
</div>
)}
{item.images.length > 0 ? (
<div className="mb-4">
<img
@@ -117,78 +126,89 @@ const ItemDetail: React.FC = () => {
</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-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 className="col-md-8">
{/* Item Name */}
<h1 className="mb-3">{item.name}</h1>
{/* Owner Info */}
{item.owner && (
<div
className="d-flex align-items-center mb-4"
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>
)}
{/* Description (no label) */}
<div className="mb-4">
<p>{item.description}</p>
</div>
)}
{item.pricePerDay && (
<div className="col-6">
<strong>Per Day:</strong> ${item.pricePerDay}
</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>
)}
{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>
<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>
{/* Map */}
<LocationMap
latitude={item.latitude}
longitude={item.longitude}
@@ -196,6 +216,9 @@ const ItemDetail: React.FC = () => {
itemName={item.name}
/>
<ItemReviews itemId={item.id} />
{/* Rules */}
{item.rules && (
<div className="mb-4">
<h5>Rules</h5>
@@ -203,30 +226,9 @@ const ItemDetail: React.FC = () => {
</div>
)}
<ItemReviews itemId={item.id} />
<div className="d-flex gap-2 mb-5">
{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>
{/* Replacement Cost (under Rules) */}
<div className="mb-4">
<p><strong>Replacement Cost:</strong> ${item.replacementCost}</p>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import api from "../services/api";
import { Item, Rental } from "../types";
@@ -7,6 +7,7 @@ import { rentalAPI } from "../services/api";
const MyListings: React.FC = () => {
const { user } = useAuth();
const navigate = useNavigate();
const [listings, setListings] = useState<Item[]>([]);
const [rentals, setRentals] = useState<Rental[]>([]);
const [loading, setLoading] = useState(true);
@@ -118,7 +119,7 @@ const MyListings: React.FC = () => {
return (
<div className="container mt-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<h1>My Listings</h1>
<h1>Listings</h1>
<Link to="/create-item" className="btn btn-primary">
Add New Item
</Link>
@@ -161,20 +162,21 @@ const MyListings: React.FC = () => {
<div className="row">
{listings.map((item) => (
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
<Link
to={`/items/${item.id}/edit`}
className="text-decoration-none"
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
const target = e.target as HTMLElement;
if (
target.closest("button") ||
target.closest(".rental-requests")
) {
e.preventDefault();
}
}}
>
<div className="card h-100" style={{ cursor: "pointer" }}>
<div
className="card h-100"
style={{ cursor: "pointer" }}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (
target.closest("button") ||
target.closest("a") ||
target.closest(".rental-requests")
) {
return;
}
navigate(`/items/${item.id}`);
}}
>
{item.images && item.images[0] && (
<img
src={item.images[0]}
@@ -213,6 +215,12 @@ const MyListings: React.FC = () => {
</div>
<div className="d-flex gap-2">
<Link
to={`/items/${item.id}/edit`}
className="btn btn-sm btn-outline-primary"
>
Edit
</Link>
<button
onClick={() => toggleAvailability(item)}
className="btn btn-sm btn-outline-info"
@@ -354,7 +362,6 @@ const MyListings: React.FC = () => {
})()}
</div>
</div>
</Link>
</div>
))}
</div>

View File

@@ -85,6 +85,18 @@ export interface Item {
startTime?: string;
endTime?: string;
}>;
availableAfter?: string;
availableBefore?: string;
specifyTimesPerDay?: boolean;
weeklyTimes?: {
sunday: { availableAfter: string; availableBefore: string };
monday: { availableAfter: string; availableBefore: string };
tuesday: { availableAfter: string; availableBefore: string };
wednesday: { availableAfter: string; availableBefore: string };
thursday: { availableAfter: string; availableBefore: string };
friday: { availableAfter: string; availableBefore: string };
saturday: { availableAfter: string; availableBefore: string };
};
ownerId: string;
owner?: User;
createdAt: string;