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,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;

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>

View File

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

View File

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

View File

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

View File

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