streamlined address and availability
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface AvailabilityData {
|
||||
generalAvailableAfter: string;
|
||||
@@ -18,31 +18,35 @@ interface AvailabilityData {
|
||||
interface AvailabilitySettingsProps {
|
||||
data: AvailabilityData;
|
||||
onChange: (field: string, value: string | boolean) => void;
|
||||
onWeeklyTimeChange: (day: string, field: 'availableAfter' | 'availableBefore', value: string) => void;
|
||||
showTitle?: boolean;
|
||||
onWeeklyTimeChange: (
|
||||
day: string,
|
||||
field: "availableAfter" | "availableBefore",
|
||||
value: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
data,
|
||||
onChange,
|
||||
onWeeklyTimeChange,
|
||||
showTitle = true
|
||||
}) => {
|
||||
const generateTimeOptions = () => {
|
||||
const options = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const time24 = `${hour.toString().padStart(2, '0')}:00`;
|
||||
const time24 = `${hour.toString().padStart(2, "0")}:00`;
|
||||
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
||||
const period = hour < 12 ? 'AM' : 'PM';
|
||||
const period = hour < 12 ? "AM" : "PM";
|
||||
const time12 = `${hour12}:00 ${period}`;
|
||||
options.push({ value: time24, label: time12 });
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleGeneralChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const handleGeneralChange = (
|
||||
e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
if (type === 'checkbox') {
|
||||
if (type === "checkbox") {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
onChange(name, checked);
|
||||
} else {
|
||||
@@ -52,8 +56,6 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showTitle && <h5 className="card-title">Availability</h5>}
|
||||
|
||||
{/* General Times */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
@@ -129,7 +131,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
className="form-select form-select-sm"
|
||||
value={times.availableAfter}
|
||||
onChange={(e) =>
|
||||
onWeeklyTimeChange(day, 'availableAfter', e.target.value)
|
||||
onWeeklyTimeChange(day, "availableAfter", e.target.value)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
@@ -144,7 +146,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
className="form-select form-select-sm"
|
||||
value={times.availableBefore}
|
||||
onChange={(e) =>
|
||||
onWeeklyTimeChange(day, 'availableBefore', e.target.value)
|
||||
onWeeklyTimeChange(day, "availableBefore", e.target.value)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
@@ -162,4 +164,4 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailabilitySettings;
|
||||
export default AvailabilitySettings;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,9 +14,9 @@ interface ItemFormData {
|
||||
localDeliveryRadius?: number;
|
||||
shippingAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number;
|
||||
pricePerDay?: number;
|
||||
replacementCost: number;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
replacementCost: number | string;
|
||||
location: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
@@ -146,7 +146,7 @@ const EditItem: 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 }));
|
||||
@@ -163,6 +163,9 @@ const EditItem: React.FC = () => {
|
||||
|
||||
await itemAPI.updateItem(id!, {
|
||||
...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,
|
||||
images: imageUrls,
|
||||
});
|
||||
|
||||
@@ -519,9 +522,12 @@ const EditItem: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="replacementCost" className="form-label">
|
||||
<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
|
||||
@@ -533,12 +539,10 @@ const EditItem: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
The cost to replace the item if lost
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { userAPI, itemAPI, rentalAPI } from "../services/api";
|
||||
import { User, Item, Rental } from "../types";
|
||||
import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api";
|
||||
import { User, Item, Rental, Address } from "../types";
|
||||
import { getImageUrl } from "../utils/imageUrl";
|
||||
import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
|
||||
@@ -11,7 +11,7 @@ const Profile: React.FC = () => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
||||
const [activeSection, setActiveSection] = useState<string>("overview");
|
||||
const [profileData, setProfileData] = useState<User | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
@@ -47,12 +47,46 @@ const Profile: React.FC = () => {
|
||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
},
|
||||
});
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [addressesLoading, setAddressesLoading] = useState(true);
|
||||
const [showAddressForm, setShowAddressForm] = useState(false);
|
||||
const [editingAddressId, setEditingAddressId] = useState<string | null>(null);
|
||||
const [addressFormData, setAddressFormData] = useState({
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile();
|
||||
fetchStats();
|
||||
fetchUserAddresses();
|
||||
fetchUserAvailability();
|
||||
}, []);
|
||||
|
||||
const fetchUserAvailability = async () => {
|
||||
try {
|
||||
const response = await userAPI.getAvailability();
|
||||
setAvailabilityData(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user availability:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserAddresses = async () => {
|
||||
try {
|
||||
const response = await addressAPI.getAddresses();
|
||||
setUserAddresses(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching addresses:", error);
|
||||
} finally {
|
||||
setAddressesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const response = await userAPI.getProfile();
|
||||
@@ -213,22 +247,180 @@ const Profile: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleAvailabilityChange = (field: string, value: string | boolean) => {
|
||||
setAvailabilityData(prev => ({ ...prev, [field]: value }));
|
||||
setAvailabilityData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleWeeklyTimeChange = (day: string, field: 'availableAfter' | 'availableBefore', value: string) => {
|
||||
setAvailabilityData(prev => ({
|
||||
const handleWeeklyTimeChange = (
|
||||
day: string,
|
||||
field: "availableAfter" | "availableBefore",
|
||||
value: string
|
||||
) => {
|
||||
setAvailabilityData((prev) => ({
|
||||
...prev,
|
||||
weeklyTimes: {
|
||||
...prev.weeklyTimes,
|
||||
[day]: {
|
||||
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveAvailability = async () => {
|
||||
try {
|
||||
await userAPI.updateAvailability(availabilityData);
|
||||
setSuccess("Availability settings saved successfully");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (error) {
|
||||
console.error("Error saving availability:", error);
|
||||
setError("Failed to save availability settings");
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddressDisplay = (address: Address) => {
|
||||
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
|
||||
};
|
||||
|
||||
const handleDeleteAddress = async (addressId: string) => {
|
||||
try {
|
||||
await addressAPI.deleteAddress(addressId);
|
||||
setUserAddresses((prev) => prev.filter((addr) => addr.id !== addressId));
|
||||
} catch (error) {
|
||||
console.error("Error deleting address:", error);
|
||||
setError("Failed to delete address");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAddress = () => {
|
||||
setAddressFormData({
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
});
|
||||
setEditingAddressId(null);
|
||||
setShowAddressForm(true);
|
||||
};
|
||||
|
||||
const handleEditAddress = (address: Address) => {
|
||||
setAddressFormData({
|
||||
address1: address.address1,
|
||||
address2: address.address2 || "",
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
zipCode: address.zipCode,
|
||||
country: address.country,
|
||||
});
|
||||
setEditingAddressId(address.id);
|
||||
setShowAddressForm(true);
|
||||
};
|
||||
|
||||
const handleAddressFormChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setAddressFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSaveAddress = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (editingAddressId) {
|
||||
// Update existing address
|
||||
const response = await addressAPI.updateAddress(
|
||||
editingAddressId,
|
||||
addressFormData
|
||||
);
|
||||
setUserAddresses((prev) =>
|
||||
prev.map((addr) =>
|
||||
addr.id === editingAddressId ? response.data : addr
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Create new address
|
||||
const response = await addressAPI.createAddress({
|
||||
...addressFormData,
|
||||
isPrimary: userAddresses.length === 0,
|
||||
});
|
||||
setUserAddresses((prev) => [...prev, response.data]);
|
||||
}
|
||||
setShowAddressForm(false);
|
||||
setEditingAddressId(null);
|
||||
} catch (error) {
|
||||
console.error("Error saving address:", error);
|
||||
setError("Failed to save address");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelAddressForm = () => {
|
||||
setShowAddressForm(false);
|
||||
setEditingAddressId(null);
|
||||
setAddressFormData({
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
});
|
||||
};
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
@@ -244,7 +436,7 @@ const Profile: React.FC = () => {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1 className="mb-4">Profile</h1>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
@@ -263,43 +455,55 @@ const Profile: React.FC = () => {
|
||||
<div className="card">
|
||||
<div className="list-group list-group-flush">
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'overview' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('overview')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "overview" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("overview")}
|
||||
>
|
||||
<i className="bi bi-person-circle me-2"></i>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'owner-settings' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('owner-settings')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "owner-settings" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("owner-settings")}
|
||||
>
|
||||
<i className="bi bi-gear me-2"></i>
|
||||
Owner Settings
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'personal-info' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('personal-info')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "personal-info" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("personal-info")}
|
||||
>
|
||||
<i className="bi bi-person me-2"></i>
|
||||
Personal Information
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'notifications' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('notifications')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "notifications" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("notifications")}
|
||||
>
|
||||
<i className="bi bi-bell me-2"></i>
|
||||
Notification Settings
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'privacy' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('privacy')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "privacy" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("privacy")}
|
||||
>
|
||||
<i className="bi bi-shield-lock me-2"></i>
|
||||
Privacy & Security
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'payment' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('payment')}
|
||||
className={`list-group-item list-group-item-action ${
|
||||
activeSection === "payment" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSection("payment")}
|
||||
>
|
||||
<i className="bi bi-credit-card me-2"></i>
|
||||
Payment Methods
|
||||
@@ -318,154 +522,163 @@ const Profile: React.FC = () => {
|
||||
{/* Right Content Area */}
|
||||
<div className="col-md-9">
|
||||
{/* Overview Section */}
|
||||
{activeSection === 'overview' && (
|
||||
{activeSection === "overview" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Overview</h4>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="text-center">
|
||||
<div className="position-relative d-inline-block mb-3">
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile"
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="text-center">
|
||||
<div className="position-relative d-inline-block mb-3">
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile"
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||
style={{ width: "120px", height: "120px" }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-person-fill text-white"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<label
|
||||
htmlFor="profileImageOverview"
|
||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||
style={{
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
padding: "0",
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-camera-fill"></i>
|
||||
<input
|
||||
type="file"
|
||||
id="profileImageOverview"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="d-none"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div>
|
||||
<div className="row justify-content-center mb-3">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
placeholder="First Name"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||
style={{ width: "120px", height: "120px" }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-person-fill text-white"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<label
|
||||
htmlFor="profileImageOverview"
|
||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||
style={{ width: "35px", height: "35px", padding: "0" }}
|
||||
>
|
||||
<i className="bi bi-camera-fill"></i>
|
||||
<input
|
||||
type="file"
|
||||
id="profileImageOverview"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="d-none"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
placeholder="Last Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div>
|
||||
<div className="row justify-content-center mb-3">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
placeholder="First Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
placeholder="Last Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-2 justify-content-center">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h5>
|
||||
{profileData?.firstName} {profileData?.lastName}
|
||||
</h5>
|
||||
<p className="text-muted">@{profileData?.username}</p>
|
||||
{profileData?.isVerified && (
|
||||
<span className="badge bg-success mb-3">
|
||||
<i className="bi bi-check-circle-fill"></i> Verified
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<i className="bi bi-pencil me-2"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-2 justify-content-center">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h5>
|
||||
{profileData?.firstName} {profileData?.lastName}
|
||||
</h5>
|
||||
<p className="text-muted">@{profileData?.username}</p>
|
||||
{profileData?.isVerified && (
|
||||
<span className="badge bg-success mb-3">
|
||||
<i className="bi bi-check-circle-fill"></i>{" "}
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<i className="bi bi-pencil me-2"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Account Statistics</h5>
|
||||
<div className="row text-center">
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-primary mb-1">{stats.itemsListed}</h4>
|
||||
<h6 className="text-muted">Items Listed</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-success mb-1">{stats.acceptedRentals}</h4>
|
||||
<h6 className="text-muted">Active Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-info mb-1">{stats.totalRentals}</h4>
|
||||
<h6 className="text-muted">Total Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Account Statistics</h5>
|
||||
<div className="row text-center">
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-primary mb-1">
|
||||
{stats.itemsListed}
|
||||
</h4>
|
||||
<h6 className="text-muted">Items Listed</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-success mb-1">
|
||||
{stats.acceptedRentals}
|
||||
</h4>
|
||||
<h6 className="text-muted">Active Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-info mb-1">{stats.totalRentals}</h4>
|
||||
<h6 className="text-muted">Total Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Personal Information Section */}
|
||||
{activeSection === 'personal-info' && (
|
||||
{activeSection === "personal-info" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Personal Information</h4>
|
||||
<div className="card">
|
||||
@@ -531,70 +744,278 @@ const Profile: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Owner Settings Section */}
|
||||
{activeSection === 'owner-settings' && (
|
||||
{activeSection === "owner-settings" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Owner Settings</h4>
|
||||
|
||||
{/* Addresses Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Saved Addresses</h5>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
<>
|
||||
{userAddresses.length === 0 && !showAddressForm ? (
|
||||
<div className="text-center py-3">
|
||||
<p className="text-muted">No saved addresses yet</p>
|
||||
<small className="text-muted">
|
||||
Add an address or create your first listing to save
|
||||
one automatically
|
||||
</small>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{userAddresses.length > 0 && !showAddressForm && (
|
||||
<>
|
||||
<div className="list-group list-group-flush mb-3">
|
||||
{userAddresses.map((address) => (
|
||||
<div
|
||||
key={address.id}
|
||||
className="list-group-item d-flex justify-content-between align-items-start"
|
||||
>
|
||||
<div className="flex-grow-1">
|
||||
<div className="fw-medium">
|
||||
{formatAddressDisplay(address)}
|
||||
</div>
|
||||
{address.address2 && (
|
||||
<small className="text-muted">
|
||||
{address.address2}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="btn-group">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() =>
|
||||
handleEditAddress(address)
|
||||
}
|
||||
>
|
||||
<i className="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() =>
|
||||
handleDeleteAddress(address.id)
|
||||
}
|
||||
>
|
||||
<i className="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handleAddAddress}
|
||||
>
|
||||
Add New Address
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Show Add New Address button even when no addresses exist */}
|
||||
{userAddresses.length === 0 && !showAddressForm && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handleAddAddress}
|
||||
>
|
||||
Add New Address
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Address Form */}
|
||||
{showAddressForm && (
|
||||
<form onSubmit={handleSaveAddress}>
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label
|
||||
htmlFor="addressFormAddress1"
|
||||
className="form-label"
|
||||
>
|
||||
Address Line 1 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="addressFormAddress1"
|
||||
name="address1"
|
||||
value={addressFormData.address1}
|
||||
onChange={handleAddressFormChange}
|
||||
placeholder=""
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label
|
||||
htmlFor="addressFormAddress2"
|
||||
className="form-label"
|
||||
>
|
||||
Address Line 2
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="addressFormAddress2"
|
||||
name="address2"
|
||||
value={addressFormData.address2}
|
||||
onChange={handleAddressFormChange}
|
||||
placeholder="Apt, Suite, Unit, etc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label
|
||||
htmlFor="addressFormCity"
|
||||
className="form-label"
|
||||
>
|
||||
City *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="addressFormCity"
|
||||
name="city"
|
||||
value={addressFormData.city}
|
||||
onChange={handleAddressFormChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label
|
||||
htmlFor="addressFormState"
|
||||
className="form-label"
|
||||
>
|
||||
State *
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="addressFormState"
|
||||
name="state"
|
||||
value={addressFormData.state}
|
||||
onChange={handleAddressFormChange}
|
||||
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="addressFormZipCode"
|
||||
className="form-label"
|
||||
>
|
||||
ZIP Code *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="addressFormZipCode"
|
||||
name="zipCode"
|
||||
value={addressFormData.zipCode}
|
||||
onChange={handleAddressFormChange}
|
||||
placeholder="12345"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{editingAddressId
|
||||
? "Update Address"
|
||||
: "Save Address"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancelAddressForm}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Availability Card */}
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{/* Addresses Section */}
|
||||
<div className="mb-5">
|
||||
<h5 className="mb-3">Saved Addresses</h5>
|
||||
<p className="text-muted small mb-3">Manage addresses for your rental locations</p>
|
||||
<button className="btn btn-outline-primary">
|
||||
<i className="bi bi-plus-circle me-2"></i>
|
||||
Add New Address
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Availability Section */}
|
||||
<div>
|
||||
<h5 className="mb-3">Default Availability</h5>
|
||||
<p className="text-muted small mb-3">Set your general availability for all items</p>
|
||||
<AvailabilitySettings
|
||||
data={availabilityData}
|
||||
onChange={handleAvailabilityChange}
|
||||
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||
showTitle={false}
|
||||
/>
|
||||
<button className="btn btn-outline-success mt-3">
|
||||
<i className="bi bi-check2 me-2"></i>
|
||||
Save Availability
|
||||
</button>
|
||||
</div>
|
||||
<h5 className="card-title">Availability</h5>
|
||||
<AvailabilitySettings
|
||||
data={availabilityData}
|
||||
onChange={handleAvailabilityChange}
|
||||
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline-success mt-3"
|
||||
onClick={handleSaveAvailability}
|
||||
>
|
||||
Save Availability
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Placeholder sections for other menu items */}
|
||||
{activeSection === 'notifications' && (
|
||||
{activeSection === "notifications" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Notification Settings</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Notification preferences coming soon...</p>
|
||||
<p className="text-muted">
|
||||
Notification preferences coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'privacy' && (
|
||||
{activeSection === "privacy" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Privacy & Security</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Privacy and security settings coming soon...</p>
|
||||
<p className="text-muted">
|
||||
Privacy and security settings coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'payment' && (
|
||||
{activeSection === "payment" && (
|
||||
<div>
|
||||
<h4 className="mb-4">Payment Methods</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Payment method management coming soon...</p>
|
||||
<p className="text-muted">
|
||||
Payment method management coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,15 @@ export const userAPI = {
|
||||
},
|
||||
}),
|
||||
getPublicProfile: (id: string) => api.get(`/users/${id}`),
|
||||
getAvailability: () => api.get("/users/availability"),
|
||||
updateAvailability: (data: any) => api.put("/users/availability", data),
|
||||
};
|
||||
|
||||
export const addressAPI = {
|
||||
getAddresses: () => api.get("/users/addresses"),
|
||||
createAddress: (data: any) => api.post("/users/addresses", data),
|
||||
updateAddress: (id: string, data: any) => api.put(`/users/addresses/${id}`, data),
|
||||
deleteAddress: (id: string) => api.delete(`/users/addresses/${id}`),
|
||||
};
|
||||
|
||||
export const itemAPI = {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export interface Address {
|
||||
id: string;
|
||||
userId: string;
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
isPrimary: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
@@ -13,6 +29,7 @@ export interface User {
|
||||
country?: string;
|
||||
profileImage?: string;
|
||||
isVerified: boolean;
|
||||
addresses?: Address[];
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
|
||||
Reference in New Issue
Block a user