made components that create and edit item can share, started item detail changes, listings provide more views
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user