448 lines
14 KiB
TypeScript
448 lines
14 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
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 {
|
|
name: string;
|
|
description: string;
|
|
pickUpAvailable: boolean;
|
|
inPlaceUseAvailable: boolean;
|
|
pricePerHour?: number | string;
|
|
pricePerDay?: number | string;
|
|
replacementCost: number | string;
|
|
address1: string;
|
|
address2: string;
|
|
city: string;
|
|
state: string;
|
|
zipCode: string;
|
|
country: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
rules?: string;
|
|
minimumRentalDays: number;
|
|
needsTraining: boolean;
|
|
generalAvailableAfter: string;
|
|
generalAvailableBefore: string;
|
|
specifyTimesPerDay: boolean;
|
|
weeklyTimes: {
|
|
sunday: { availableAfter: string; availableBefore: string };
|
|
monday: { availableAfter: string; availableBefore: string };
|
|
tuesday: { availableAfter: string; availableBefore: string };
|
|
wednesday: { availableAfter: string; availableBefore: string };
|
|
thursday: { availableAfter: string; availableBefore: string };
|
|
friday: { availableAfter: string; availableBefore: string };
|
|
saturday: { availableAfter: string; availableBefore: string };
|
|
};
|
|
}
|
|
|
|
const CreateItem: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [formData, setFormData] = useState<ItemFormData>({
|
|
name: "",
|
|
description: "",
|
|
pickUpAvailable: false,
|
|
inPlaceUseAvailable: false,
|
|
pricePerDay: "",
|
|
replacementCost: "",
|
|
address1: "",
|
|
address2: "",
|
|
city: "",
|
|
state: "",
|
|
zipCode: "",
|
|
country: "US",
|
|
minimumRentalDays: 1,
|
|
needsTraining: false,
|
|
generalAvailableAfter: "09:00",
|
|
generalAvailableBefore: "17:00",
|
|
specifyTimesPerDay: false,
|
|
weeklyTimes: {
|
|
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
|
},
|
|
});
|
|
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();
|
|
if (!user) {
|
|
setError("You must be logged in to create a listing");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError("");
|
|
|
|
try {
|
|
// For now, we'll store image URLs as base64 strings
|
|
// In production, you'd upload to a service like S3
|
|
const imageUrls = imagePreviews;
|
|
|
|
// Construct location from address components
|
|
const locationParts = [
|
|
formData.address1,
|
|
formData.address2,
|
|
formData.city,
|
|
formData.state,
|
|
formData.zipCode,
|
|
formData.country,
|
|
].filter((part) => part && part.trim());
|
|
|
|
const location = locationParts.join(", ");
|
|
|
|
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,
|
|
availableAfter: formData.generalAvailableAfter,
|
|
availableBefore: formData.generalAvailableBefore,
|
|
specifyTimesPerDay: formData.specifyTimesPerDay,
|
|
weeklyTimes: formData.weeklyTimes,
|
|
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");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (
|
|
e: React.ChangeEvent<
|
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
|
>
|
|
) => {
|
|
const { name, value, type } = e.target;
|
|
|
|
if (type === "checkbox") {
|
|
const checked = (e.target as HTMLInputElement).checked;
|
|
setFormData((prev) => ({ ...prev, [name]: checked }));
|
|
} else if (type === "number") {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[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 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,
|
|
},
|
|
},
|
|
}));
|
|
};
|
|
|
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(e.target.files || []);
|
|
|
|
// Limit to 5 images
|
|
if (imageFiles.length + files.length > 5) {
|
|
setError("You can upload a maximum of 5 images");
|
|
return;
|
|
}
|
|
|
|
const newImageFiles = [...imageFiles, ...files];
|
|
setImageFiles(newImageFiles);
|
|
|
|
// Create previews
|
|
files.forEach((file) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setImagePreviews((prev) => [...prev, reader.result as string]);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
};
|
|
|
|
const removeImage = (index: number) => {
|
|
setImageFiles((prev) => prev.filter((_, i) => i !== index));
|
|
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
|
|
};
|
|
|
|
return (
|
|
<div className="container mt-4">
|
|
<div className="row justify-content-center">
|
|
<div className="col-md-8">
|
|
<h1>List an Item for Rent</h1>
|
|
|
|
{error && (
|
|
<div className="alert alert-danger" role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<ImageUpload
|
|
imageFiles={imageFiles}
|
|
imagePreviews={imagePreviews}
|
|
onImageChange={handleImageChange}
|
|
onRemoveImage={removeImage}
|
|
error={error}
|
|
/>
|
|
|
|
<ItemInformation
|
|
name={formData.name}
|
|
description={formData.description}
|
|
onChange={handleChange}
|
|
/>
|
|
|
|
<LocationForm
|
|
data={{
|
|
address1: formData.address1,
|
|
address2: formData.address2,
|
|
city: formData.city,
|
|
state: formData.state,
|
|
zipCode: formData.zipCode,
|
|
country: formData.country,
|
|
latitude: formData.latitude,
|
|
longitude: formData.longitude,
|
|
}}
|
|
userAddresses={userAddresses}
|
|
selectedAddressId={selectedAddressId}
|
|
addressesLoading={addressesLoading}
|
|
onChange={handleChange}
|
|
onAddressSelect={handleAddressSelect}
|
|
formatAddressDisplay={formatAddressDisplay}
|
|
/>
|
|
|
|
<DeliveryOptions
|
|
pickUpAvailable={formData.pickUpAvailable}
|
|
inPlaceUseAvailable={formData.inPlaceUseAvailable}
|
|
onChange={handleChange}
|
|
/>
|
|
|
|
{/* Availability Card */}
|
|
<div className="card mb-4">
|
|
<div className="card-body">
|
|
<AvailabilitySettings
|
|
data={{
|
|
generalAvailableAfter: formData.generalAvailableAfter,
|
|
generalAvailableBefore: formData.generalAvailableBefore,
|
|
specifyTimesPerDay: formData.specifyTimesPerDay,
|
|
weeklyTimes: formData.weeklyTimes,
|
|
}}
|
|
onChange={(field, value) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
}}
|
|
onWeeklyTimeChange={handleWeeklyTimeChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<PricingForm
|
|
priceType={priceType}
|
|
pricePerHour={formData.pricePerHour || ""}
|
|
pricePerDay={formData.pricePerDay || ""}
|
|
replacementCost={formData.replacementCost}
|
|
minimumRentalDays={formData.minimumRentalDays}
|
|
onPriceTypeChange={setPriceType}
|
|
onChange={handleChange}
|
|
/>
|
|
|
|
<RulesForm
|
|
needsTraining={formData.needsTraining}
|
|
rules={formData.rules || ""}
|
|
onChange={handleChange}
|
|
/>
|
|
|
|
<div className="d-grid gap-2 mb-5">
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary"
|
|
disabled={loading}
|
|
>
|
|
{loading ? "Creating..." : "Create Listing"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
onClick={() => navigate(-1)}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CreateItem;
|