streamlined address and availability

This commit is contained in:
jackiettran
2025-08-20 14:56:16 -04:00
parent 66dc187295
commit ddd27a59f9
10 changed files with 1173 additions and 314 deletions

View File

@@ -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>