streamlined address and availability
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import api from "../services/api";
|
||||
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
|
||||
import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
import { Address } from "../types";
|
||||
|
||||
interface ItemFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
pickUpAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number;
|
||||
pricePerDay?: number;
|
||||
replacementCost: number;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
replacementCost: number | string;
|
||||
location: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
@@ -48,8 +49,8 @@ const CreateItem: React.FC = () => {
|
||||
description: "",
|
||||
pickUpAvailable: false,
|
||||
inPlaceUseAvailable: false,
|
||||
pricePerDay: undefined,
|
||||
replacementCost: 0,
|
||||
pricePerDay: "",
|
||||
replacementCost: "",
|
||||
location: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
@@ -75,6 +76,65 @@ const CreateItem: React.FC = () => {
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
||||
const [addressesLoading, setAddressesLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserAddresses();
|
||||
fetchUserAvailability();
|
||||
}, []);
|
||||
|
||||
const fetchUserAvailability = async () => {
|
||||
try {
|
||||
const response = await userAPI.getAvailability();
|
||||
const userAvailability = response.data;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
generalAvailableAfter: userAvailability.generalAvailableAfter,
|
||||
generalAvailableBefore: userAvailability.generalAvailableBefore,
|
||||
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
|
||||
weeklyTimes: userAvailability.weeklyTimes
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching user availability:', error);
|
||||
// Use default values if fetch fails
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-populate if user has exactly one address and addresses have finished loading
|
||||
if (
|
||||
!addressesLoading &&
|
||||
userAddresses.length === 1 &&
|
||||
selectedAddressId === ""
|
||||
) {
|
||||
const address = userAddresses[0];
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address1: address.address1,
|
||||
address2: address.address2 || "",
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
zipCode: address.zipCode,
|
||||
country: address.country,
|
||||
latitude: address.latitude,
|
||||
longitude: address.longitude,
|
||||
}));
|
||||
setSelectedAddressId(address.id);
|
||||
}
|
||||
}, [userAddresses, addressesLoading]);
|
||||
|
||||
const fetchUserAddresses = async () => {
|
||||
try {
|
||||
const response = await addressAPI.getAddresses();
|
||||
setUserAddresses(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching addresses:", error);
|
||||
} finally {
|
||||
setAddressesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -105,9 +165,52 @@ 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,
|
||||
location,
|
||||
images: imageUrls,
|
||||
});
|
||||
|
||||
// Auto-save address if user has no addresses and entered manual address
|
||||
if (userAddresses.length === 0 && formData.address1) {
|
||||
try {
|
||||
await addressAPI.createAddress({
|
||||
address1: formData.address1,
|
||||
address2: formData.address2,
|
||||
city: formData.city,
|
||||
state: formData.state,
|
||||
zipCode: formData.zipCode,
|
||||
country: formData.country,
|
||||
latitude: formData.latitude,
|
||||
longitude: formData.longitude,
|
||||
isPrimary: true,
|
||||
});
|
||||
} catch (addressError) {
|
||||
console.error("Failed to save address:", addressError);
|
||||
// Don't fail item creation if address save fails
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is user's first item and save availability settings
|
||||
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
|
||||
});
|
||||
}
|
||||
} catch (availabilityError) {
|
||||
console.error("Failed to save availability:", availabilityError);
|
||||
// Don't fail item creation if availability save fails
|
||||
}
|
||||
|
||||
navigate(`/items/${response.data.id}`);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || "Failed to create listing");
|
||||
@@ -129,13 +232,104 @@ const CreateItem: React.FC = () => {
|
||||
} else if (type === "number") {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value ? parseFloat(value) : undefined,
|
||||
[name]: value === "" ? "" : parseFloat(value) || 0,
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressSelect = (addressId: string) => {
|
||||
if (addressId === "new") {
|
||||
// Clear form for new address entry
|
||||
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 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",
|
||||
@@ -153,7 +347,6 @@ const CreateItem: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
|
||||
@@ -285,104 +478,134 @@ const CreateItem: React.FC = () => {
|
||||
<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
|
||||
This address is private. It will only be used to show
|
||||
renters a general area.
|
||||
</small>
|
||||
</div>
|
||||
<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="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={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
|
||||
/>
|
||||
{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>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="state" className="form-label">
|
||||
State *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="state"
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
placeholder="CA"
|
||||
required
|
||||
/>
|
||||
</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="12345"
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="country" className="form-label">
|
||||
Country *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="country"
|
||||
name="country"
|
||||
value={formData.country}
|
||||
onChange={handleChange}
|
||||
placeholder="United States"
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -438,10 +661,10 @@ const CreateItem: React.FC = () => {
|
||||
generalAvailableAfter: formData.generalAvailableAfter,
|
||||
generalAvailableBefore: formData.generalAvailableBefore,
|
||||
specifyTimesPerDay: formData.specifyTimesPerDay,
|
||||
weeklyTimes: formData.weeklyTimes
|
||||
weeklyTimes: formData.weeklyTimes,
|
||||
}}
|
||||
onChange={(field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
}}
|
||||
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||
/>
|
||||
@@ -532,6 +755,7 @@ const CreateItem: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user