made components that create and edit item can share, started item detail changes, listings provide more views
This commit is contained in:
@@ -111,6 +111,30 @@ const Item = sequelize.define('Item', {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
availableAfter: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: '09:00'
|
||||
},
|
||||
availableBefore: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: '17:00'
|
||||
},
|
||||
specifyTimesPerDay: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
weeklyTimes: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {
|
||||
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" }
|
||||
}
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
|
||||
60
frontend/src/components/DeliveryOptions.tsx
Normal file
60
frontend/src/components/DeliveryOptions.tsx
Normal 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;
|
||||
70
frontend/src/components/ImageUpload.tsx
Normal file
70
frontend/src/components/ImageUpload.tsx
Normal 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;
|
||||
53
frontend/src/components/ItemInformation.tsx
Normal file
53
frontend/src/components/ItemInformation.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
179
frontend/src/components/LocationForm.tsx
Normal file
179
frontend/src/components/LocationForm.tsx
Normal 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;
|
||||
102
frontend/src/components/PricingForm.tsx
Normal file
102
frontend/src/components/PricingForm.tsx
Normal 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;
|
||||
47
frontend/src/components/RulesForm.tsx
Normal file
47
frontend/src/components/RulesForm.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user