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,
|
type: DataTypes.JSONB,
|
||||||
defaultValue: []
|
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: {
|
ownerId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
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 React, { useState, useEffect } from "react";
|
||||||
import { Rental } from '../types';
|
import { Rental } from "../types";
|
||||||
import { rentalAPI } from '../services/api';
|
import { rentalAPI } from "../services/api";
|
||||||
|
|
||||||
interface ItemReviewsProps {
|
interface ItemReviewsProps {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -23,8 +23,9 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
|||||||
|
|
||||||
// Filter for completed rentals with reviews for this specific item
|
// Filter for completed rentals with reviews for this specific item
|
||||||
const itemReviews = allRentals.filter(
|
const itemReviews = allRentals.filter(
|
||||||
rental => rental.itemId === itemId &&
|
(rental) =>
|
||||||
rental.status === 'completed' &&
|
rental.itemId === itemId &&
|
||||||
|
rental.status === "completed" &&
|
||||||
rental.rating &&
|
rental.rating &&
|
||||||
rental.review
|
rental.review
|
||||||
);
|
);
|
||||||
@@ -37,7 +38,7 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
|||||||
setAverageRating(sum / itemReviews.length);
|
setAverageRating(sum / itemReviews.length);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch reviews:', error);
|
console.error("Failed to fetch reviews:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
|||||||
{[1, 2, 3, 4, 5].map((star) => (
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
<i
|
<i
|
||||||
key={star}
|
key={star}
|
||||||
className={`bi ${star <= rating ? 'bi-star-fill' : 'bi-star'}`}
|
className={`bi ${star <= rating ? "bi-star-fill" : "bi-star"}`}
|
||||||
></i>
|
></i>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
@@ -74,14 +75,16 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
|||||||
<h5>Reviews</h5>
|
<h5>Reviews</h5>
|
||||||
|
|
||||||
{reviews.length === 0 ? (
|
{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="mb-3">
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
{renderStars(Math.round(averageRating))}
|
{renderStars(Math.round(averageRating))}
|
||||||
<span className="fw-bold">{averageRating.toFixed(1)}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,18 +99,27 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
|||||||
src={rental.renter.profileImage}
|
src={rental.renter.profileImage}
|
||||||
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
|
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
|
||||||
className="rounded-circle"
|
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"
|
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>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong>
|
<strong>
|
||||||
|
{rental.renter?.firstName} {rental.renter?.lastName}
|
||||||
|
</strong>
|
||||||
<div className="small">
|
<div className="small">
|
||||||
{renderStars(rental.rating || 0)}
|
{renderStars(rental.rating || 0)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
|
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
|
||||||
import AvailabilitySettings from "../components/AvailabilitySettings";
|
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";
|
import { Address } from "../types";
|
||||||
|
|
||||||
interface ItemFormData {
|
interface ItemFormData {
|
||||||
@@ -89,15 +95,15 @@ const CreateItem: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await userAPI.getAvailability();
|
const response = await userAPI.getAvailability();
|
||||||
const userAvailability = response.data;
|
const userAvailability = response.data;
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
generalAvailableAfter: userAvailability.generalAvailableAfter,
|
generalAvailableAfter: userAvailability.generalAvailableAfter,
|
||||||
generalAvailableBefore: userAvailability.generalAvailableBefore,
|
generalAvailableBefore: userAvailability.generalAvailableBefore,
|
||||||
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
|
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
|
||||||
weeklyTimes: userAvailability.weeklyTimes
|
weeklyTimes: userAvailability.weeklyTimes,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching user availability:', error);
|
console.error("Error fetching user availability:", error);
|
||||||
// Use default values if fetch fails
|
// Use default values if fetch fails
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -165,9 +171,19 @@ const CreateItem: React.FC = () => {
|
|||||||
|
|
||||||
const response = await api.post("/items", {
|
const response = await api.post("/items", {
|
||||||
...formData,
|
...formData,
|
||||||
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
|
pricePerDay: formData.pricePerDay
|
||||||
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
|
? parseFloat(formData.pricePerDay.toString())
|
||||||
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
|
: 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,
|
location,
|
||||||
images: imageUrls,
|
images: imageUrls,
|
||||||
});
|
});
|
||||||
@@ -203,7 +219,7 @@ const CreateItem: React.FC = () => {
|
|||||||
generalAvailableAfter: formData.generalAvailableAfter,
|
generalAvailableAfter: formData.generalAvailableAfter,
|
||||||
generalAvailableBefore: formData.generalAvailableBefore,
|
generalAvailableBefore: formData.generalAvailableBefore,
|
||||||
specifyTimesPerDay: formData.specifyTimesPerDay,
|
specifyTimesPerDay: formData.specifyTimesPerDay,
|
||||||
weeklyTimes: formData.weeklyTimes
|
weeklyTimes: formData.weeklyTimes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (availabilityError) {
|
} catch (availabilityError) {
|
||||||
@@ -277,59 +293,6 @@ const CreateItem: React.FC = () => {
|
|||||||
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
|
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 = (
|
const handleWeeklyTimeChange = (
|
||||||
day: string,
|
day: string,
|
||||||
field: "availableAfter" | "availableBefore",
|
field: "availableAfter" | "availableBefore",
|
||||||
@@ -387,271 +350,44 @@ const CreateItem: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Images Card */}
|
<ImageUpload
|
||||||
<div className="card mb-4">
|
imageFiles={imageFiles}
|
||||||
<div className="card-body">
|
imagePreviews={imagePreviews}
|
||||||
<div className="mb-3">
|
onImageChange={handleImageChange}
|
||||||
<label className="form-label mb-0">
|
onRemoveImage={removeImage}
|
||||||
Upload Images (Max 5)
|
error={error}
|
||||||
</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>
|
|
||||||
|
|
||||||
{imagePreviews.length > 0 && (
|
<ItemInformation
|
||||||
<div className="row mt-3">
|
name={formData.name}
|
||||||
{imagePreviews.map((preview, index) => (
|
description={formData.description}
|
||||||
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
onChange={handleChange}
|
||||||
<div className="position-relative">
|
/>
|
||||||
<img
|
|
||||||
src={preview}
|
<LocationForm
|
||||||
alt={`Preview ${index + 1}`}
|
data={{
|
||||||
className="img-fluid rounded"
|
address1: formData.address1,
|
||||||
style={{
|
address2: formData.address2,
|
||||||
width: "100%",
|
city: formData.city,
|
||||||
height: "150px",
|
state: formData.state,
|
||||||
objectFit: "cover",
|
zipCode: formData.zipCode,
|
||||||
|
country: formData.country,
|
||||||
|
latitude: formData.latitude,
|
||||||
|
longitude: formData.longitude,
|
||||||
}}
|
}}
|
||||||
|
userAddresses={userAddresses}
|
||||||
|
selectedAddressId={selectedAddressId}
|
||||||
|
addressesLoading={addressesLoading}
|
||||||
|
onChange={handleChange}
|
||||||
|
onAddressSelect={handleAddressSelect}
|
||||||
|
formatAddressDisplay={formatAddressDisplay}
|
||||||
/>
|
/>
|
||||||
<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 */}
|
<DeliveryOptions
|
||||||
<div className="card mb-4">
|
pickUpAvailable={formData.pickUpAvailable}
|
||||||
<div className="card-body">
|
inPlaceUseAvailable={formData.inPlaceUseAvailable}
|
||||||
<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">
|
|
||||||
<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}
|
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>
|
|
||||||
|
|
||||||
{/* Availability Card */}
|
{/* Availability Card */}
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
@@ -671,128 +407,21 @@ const CreateItem: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pricing Card */}
|
<PricingForm
|
||||||
<div className="card mb-4">
|
priceType={priceType}
|
||||||
<div className="card-body">
|
pricePerHour={formData.pricePerHour || ""}
|
||||||
<div className="mb-3">
|
pricePerDay={formData.pricePerDay || ""}
|
||||||
<div className="row align-items-center">
|
replacementCost={formData.replacementCost}
|
||||||
<div className="col-auto">
|
minimumRentalDays={formData.minimumRentalDays}
|
||||||
<label className="col-form-label">Price per</label>
|
onPriceTypeChange={setPriceType}
|
||||||
</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}
|
onChange={handleChange}
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="0.00"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
<RulesForm
|
||||||
<label htmlFor="minimumRentalDays" className="form-label">
|
needsTraining={formData.needsTraining}
|
||||||
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
rules={formData.rules || ""}
|
||||||
</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}
|
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>
|
|
||||||
|
|
||||||
<div className="d-grid gap-2 mb-5">
|
<div className="d-grid gap-2 mb-5">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { Item, Rental } from "../types";
|
import { Item, Rental, Address } from "../types";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { itemAPI, rentalAPI } from "../services/api";
|
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";
|
||||||
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||||
import AddressAutocomplete from "../components/AddressAutocomplete";
|
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 {
|
interface ItemFormData {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
pickUpAvailable: boolean;
|
pickUpAvailable: boolean;
|
||||||
localDeliveryAvailable: boolean;
|
|
||||||
localDeliveryRadius?: number;
|
|
||||||
shippingAvailable: boolean;
|
|
||||||
inPlaceUseAvailable: boolean;
|
inPlaceUseAvailable: boolean;
|
||||||
pricePerHour?: number | string;
|
pricePerHour?: number | string;
|
||||||
pricePerDay?: number | string;
|
pricePerDay?: number | string;
|
||||||
replacementCost: number | string;
|
replacementCost: number | string;
|
||||||
location: string;
|
address1: string;
|
||||||
|
address2: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
zipCode: string;
|
||||||
|
country: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rules?: string;
|
rules?: string;
|
||||||
minimumRentalDays: number;
|
minimumRentalDays: number;
|
||||||
needsTraining: boolean;
|
needsTraining: boolean;
|
||||||
availability: boolean;
|
generalAvailableAfter: string;
|
||||||
unavailablePeriods?: Array<{
|
generalAvailableBefore: string;
|
||||||
id: string;
|
specifyTimesPerDay: boolean;
|
||||||
startDate: Date;
|
weeklyTimes: {
|
||||||
endDate: Date;
|
sunday: { availableAfter: string; availableBefore: string };
|
||||||
startTime?: string;
|
monday: { availableAfter: string; availableBefore: string };
|
||||||
endTime?: 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 = () => {
|
const EditItem: React.FC = () => {
|
||||||
@@ -44,29 +55,57 @@ const EditItem: React.FC = () => {
|
|||||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||||
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
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>({
|
const [formData, setFormData] = useState<ItemFormData>({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
pickUpAvailable: false,
|
pickUpAvailable: false,
|
||||||
localDeliveryAvailable: false,
|
|
||||||
shippingAvailable: false,
|
|
||||||
inPlaceUseAvailable: false,
|
inPlaceUseAvailable: false,
|
||||||
pricePerHour: undefined,
|
pricePerHour: "",
|
||||||
pricePerDay: undefined,
|
pricePerDay: "",
|
||||||
replacementCost: 0,
|
replacementCost: "",
|
||||||
location: "",
|
address1: "",
|
||||||
|
address2: "",
|
||||||
|
city: "",
|
||||||
|
state: "",
|
||||||
|
zipCode: "",
|
||||||
|
country: "US",
|
||||||
rules: "",
|
rules: "",
|
||||||
minimumRentalDays: 1,
|
minimumRentalDays: 1,
|
||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
availability: true,
|
generalAvailableAfter: "09:00",
|
||||||
unavailablePeriods: [],
|
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(() => {
|
useEffect(() => {
|
||||||
fetchItem();
|
fetchItem();
|
||||||
fetchAcceptedRentals();
|
fetchAcceptedRentals();
|
||||||
|
fetchUserAddresses();
|
||||||
}, [id]);
|
}, [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 () => {
|
const fetchItem = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await itemAPI.getItem(id!);
|
const response = await itemAPI.getItem(id!);
|
||||||
@@ -89,21 +128,33 @@ const EditItem: React.FC = () => {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
pickUpAvailable: item.pickUpAvailable || false,
|
pickUpAvailable: item.pickUpAvailable || false,
|
||||||
localDeliveryAvailable: item.localDeliveryAvailable || false,
|
|
||||||
localDeliveryRadius: item.localDeliveryRadius || 25,
|
|
||||||
shippingAvailable: item.shippingAvailable || false,
|
|
||||||
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
||||||
pricePerHour: item.pricePerHour,
|
pricePerHour: item.pricePerHour || "",
|
||||||
pricePerDay: item.pricePerDay,
|
pricePerDay: item.pricePerDay || "",
|
||||||
replacementCost: item.replacementCost,
|
replacementCost: item.replacementCost || "",
|
||||||
location: item.location,
|
address1: item.address1 || "",
|
||||||
|
address2: item.address2 || "",
|
||||||
|
city: item.city || "",
|
||||||
|
state: item.state || "",
|
||||||
|
zipCode: item.zipCode || "",
|
||||||
|
country: item.country || "US",
|
||||||
latitude: item.latitude,
|
latitude: item.latitude,
|
||||||
longitude: item.longitude,
|
longitude: item.longitude,
|
||||||
rules: item.rules || "",
|
rules: item.rules || "",
|
||||||
minimumRentalDays: item.minimumRentalDays,
|
minimumRentalDays: item.minimumRentalDays,
|
||||||
needsTraining: item.needsTraining || false,
|
needsTraining: item.needsTraining || false,
|
||||||
availability: item.availability,
|
generalAvailableAfter: item.availableAfter || "09:00",
|
||||||
unavailablePeriods: item.unavailablePeriods || [],
|
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
|
// Set existing images as previews
|
||||||
@@ -163,12 +214,43 @@ const EditItem: React.FC = () => {
|
|||||||
|
|
||||||
await itemAPI.updateItem(id!, {
|
await itemAPI.updateItem(id!, {
|
||||||
...formData,
|
...formData,
|
||||||
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
|
pricePerDay: formData.pricePerDay
|
||||||
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
|
? parseFloat(formData.pricePerDay.toString())
|
||||||
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
|
: 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,
|
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);
|
setSuccess(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(`/items/${id}`);
|
navigate(`/items/${id}`);
|
||||||
@@ -204,6 +286,60 @@ const EditItem: React.FC = () => {
|
|||||||
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mt-5">
|
<div className="container mt-5">
|
||||||
@@ -245,386 +381,79 @@ const EditItem: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Images Card */}
|
<ImageUpload
|
||||||
<div className="card mb-4">
|
imageFiles={imageFiles}
|
||||||
<div className="card-body">
|
imagePreviews={imagePreviews}
|
||||||
<div className="mb-3">
|
onImageChange={handleImageChange}
|
||||||
<label className="form-label">Images (Max 5)</label>
|
onRemoveImage={removeImage}
|
||||||
<input
|
error={error || ""}
|
||||||
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 && (
|
<ItemInformation
|
||||||
<div className="row mt-3">
|
name={formData.name}
|
||||||
{imagePreviews.map((preview, index) => (
|
description={formData.description}
|
||||||
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
onChange={handleChange}
|
||||||
<div className="position-relative">
|
/>
|
||||||
<img
|
|
||||||
src={preview}
|
<LocationForm
|
||||||
alt={`Preview ${index + 1}`}
|
data={{
|
||||||
className="img-fluid rounded"
|
address1: formData.address1,
|
||||||
style={{
|
address2: formData.address2,
|
||||||
width: "100%",
|
city: formData.city,
|
||||||
height: "150px",
|
state: formData.state,
|
||||||
objectFit: "cover",
|
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}
|
||||||
/>
|
/>
|
||||||
<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 mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="mb-3">
|
<AvailabilitySettings
|
||||||
<label htmlFor="name" className="form-label">
|
data={{
|
||||||
Item Name *
|
generalAvailableAfter: formData.generalAvailableAfter,
|
||||||
</label>
|
generalAvailableBefore: formData.generalAvailableBefore,
|
||||||
<input
|
specifyTimesPerDay: formData.specifyTimesPerDay,
|
||||||
type="text"
|
weeklyTimes: formData.weeklyTimes,
|
||||||
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"
|
onChange={(field, value) => {
|
||||||
required
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
/>
|
|
||||||
</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,
|
|
||||||
}));
|
|
||||||
}}
|
}}
|
||||||
mode="owner"
|
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ const ItemDetail: React.FC = () => {
|
|||||||
<div className="container mt-5">
|
<div className="container mt-5">
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
<div className="col-md-10">
|
<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 ? (
|
{item.images.length > 0 ? (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<img
|
<img
|
||||||
@@ -119,11 +128,13 @@ const ItemDetail: React.FC = () => {
|
|||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
<h1>{item.name}</h1>
|
{/* Item Name */}
|
||||||
<p className="text-muted">{item.location}</p>
|
<h1 className="mb-3">{item.name}</h1>
|
||||||
|
|
||||||
|
{/* Owner Info */}
|
||||||
{item.owner && (
|
{item.owner && (
|
||||||
<div
|
<div
|
||||||
className="d-flex align-items-center mt-2 mb-3"
|
className="d-flex align-items-center mb-4"
|
||||||
onClick={() => navigate(`/users/${item.ownerId}`)}
|
onClick={() => navigate(`/users/${item.ownerId}`)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
@@ -146,49 +157,58 @@ const ItemDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Description (no label) */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Description</h5>
|
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
{/* Right Side - Pricing Card */}
|
||||||
<h5>Pricing</h5>
|
<div className="col-md-4">
|
||||||
<div className="row">
|
<div className="card">
|
||||||
|
<div className="card-body text-center">
|
||||||
{item.pricePerHour && (
|
{item.pricePerHour && (
|
||||||
<div className="col-6">
|
<div className="mb-2">
|
||||||
<strong>Per Hour:</strong> ${item.pricePerHour}
|
<h4>${Math.floor(item.pricePerHour)}/Hour</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.pricePerDay && (
|
{item.pricePerDay && (
|
||||||
<div className="col-6">
|
<div className="mb-2">
|
||||||
<strong>Per Day:</strong> ${item.pricePerDay}
|
<h4>${Math.floor(item.pricePerDay)}/Day</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.pricePerWeek && (
|
{item.pricePerWeek && (
|
||||||
<div className="col-6">
|
<div className="mb-2">
|
||||||
<strong>Per Week:</strong> ${item.pricePerWeek}
|
<h4>${Math.floor(item.pricePerWeek)}/Week</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.pricePerMonth && (
|
{item.pricePerMonth && (
|
||||||
<div className="col-6">
|
|
||||||
<strong>Per Month:</strong> ${item.pricePerMonth}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Details</h5>
|
<h4>${Math.floor(item.pricePerMonth)}/Month</h4>
|
||||||
<p><strong>Replacement Cost:</strong> ${item.replacementCost}</p>
|
</div>
|
||||||
{item.minimumRentalDays && (
|
|
||||||
<p><strong>Minimum Rental:</strong> {item.minimumRentalDays} days</p>
|
|
||||||
)}
|
)}
|
||||||
{item.maximumRentalDays && (
|
|
||||||
<p><strong>Maximum Rental:</strong> {item.maximumRentalDays} days</p>
|
{/* Action Buttons */}
|
||||||
|
{!isOwner && item.availability && !isAlreadyRenting && (
|
||||||
|
<div className="d-grid">
|
||||||
|
<button className="btn btn-primary" onClick={handleRent}>
|
||||||
|
Rent This Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isOwner && isAlreadyRenting && (
|
||||||
|
<div className="d-grid">
|
||||||
|
<button className="btn btn-success" disabled style={{ opacity: 0.8 }}>
|
||||||
|
✓ Renting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map */}
|
||||||
<LocationMap
|
<LocationMap
|
||||||
latitude={item.latitude}
|
latitude={item.latitude}
|
||||||
longitude={item.longitude}
|
longitude={item.longitude}
|
||||||
@@ -196,6 +216,9 @@ const ItemDetail: React.FC = () => {
|
|||||||
itemName={item.name}
|
itemName={item.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ItemReviews itemId={item.id} />
|
||||||
|
|
||||||
|
{/* Rules */}
|
||||||
{item.rules && (
|
{item.rules && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Rules</h5>
|
<h5>Rules</h5>
|
||||||
@@ -203,30 +226,9 @@ const ItemDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ItemReviews itemId={item.id} />
|
{/* Replacement Cost (under Rules) */}
|
||||||
|
<div className="mb-4">
|
||||||
<div className="d-flex gap-2 mb-5">
|
<p><strong>Replacement Cost:</strong> ${item.replacementCost}</p>
|
||||||
{isOwner ? (
|
|
||||||
<button className="btn btn-primary" onClick={handleEdit}>
|
|
||||||
Edit Listing
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
item.availability && !isAlreadyRenting && (
|
|
||||||
<button className="btn btn-primary" onClick={handleRent}>
|
|
||||||
Rent This Item
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{!isOwner && isAlreadyRenting && (
|
|
||||||
<button className="btn btn-success" disabled style={{ opacity: 0.8 }}>
|
|
||||||
✓ Renting
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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 { useAuth } from "../contexts/AuthContext";
|
||||||
import api from "../services/api";
|
import api from "../services/api";
|
||||||
import { Item, Rental } from "../types";
|
import { Item, Rental } from "../types";
|
||||||
@@ -7,6 +7,7 @@ import { rentalAPI } from "../services/api";
|
|||||||
|
|
||||||
const MyListings: React.FC = () => {
|
const MyListings: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [listings, setListings] = useState<Item[]>([]);
|
const [listings, setListings] = useState<Item[]>([]);
|
||||||
const [rentals, setRentals] = useState<Rental[]>([]);
|
const [rentals, setRentals] = useState<Rental[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -118,7 +119,7 @@ const MyListings: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<div className="d-flex justify-content-between align-items-center mb-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">
|
<Link to="/create-item" className="btn btn-primary">
|
||||||
Add New Item
|
Add New Item
|
||||||
</Link>
|
</Link>
|
||||||
@@ -161,20 +162,21 @@ const MyListings: React.FC = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
{listings.map((item) => (
|
{listings.map((item) => (
|
||||||
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
|
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
|
||||||
<Link
|
<div
|
||||||
to={`/items/${item.id}/edit`}
|
className="card h-100"
|
||||||
className="text-decoration-none"
|
style={{ cursor: "pointer" }}
|
||||||
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (
|
if (
|
||||||
target.closest("button") ||
|
target.closest("button") ||
|
||||||
|
target.closest("a") ||
|
||||||
target.closest(".rental-requests")
|
target.closest(".rental-requests")
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
return;
|
||||||
}
|
}
|
||||||
|
navigate(`/items/${item.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="card h-100" style={{ cursor: "pointer" }}>
|
|
||||||
{item.images && item.images[0] && (
|
{item.images && item.images[0] && (
|
||||||
<img
|
<img
|
||||||
src={item.images[0]}
|
src={item.images[0]}
|
||||||
@@ -213,6 +215,12 @@ const MyListings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
|
<Link
|
||||||
|
to={`/items/${item.id}/edit`}
|
||||||
|
className="btn btn-sm btn-outline-primary"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleAvailability(item)}
|
onClick={() => toggleAvailability(item)}
|
||||||
className="btn btn-sm btn-outline-info"
|
className="btn btn-sm btn-outline-info"
|
||||||
@@ -354,7 +362,6 @@ const MyListings: React.FC = () => {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,6 +85,18 @@ export interface Item {
|
|||||||
startTime?: string;
|
startTime?: string;
|
||||||
endTime?: 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;
|
ownerId: string;
|
||||||
owner?: User;
|
owner?: User;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user