Files
rentall-app/frontend/src/pages/CreateItem.tsx

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;