Files
rentall-app/frontend/src/pages/Profile.tsx
jackiettran b0268a2fb7 s3
2025-12-11 20:05:18 -05:00

1638 lines
64 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api";
import { User, Item, Rental, Address } from "../types";
import { uploadFile, getPublicImageUrl } from "../services/uploadService";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ReviewItemModal from "../components/ReviewModal";
import ReviewRenterModal from "../components/ReviewRenterModal";
import ReviewDetailsModal from "../components/ReviewDetailsModal";
import {
geocodingService,
AddressComponents,
} from "../services/geocodingService";
import AddressAutocomplete from "../components/AddressAutocomplete";
import { PlaceDetails } from "../services/placesService";
import {
useAddressAutocomplete,
usStates,
} from "../hooks/useAddressAutocomplete";
const Profile: React.FC = () => {
const { user, updateUser, logout } = useAuth();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
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 [profileData, setProfileData] = useState<User | null>(null);
const [formData, setFormData] = useState<{
firstName: string;
lastName: string;
email: string;
phone: string;
address1: string;
address2: string;
city: string;
state: string;
zipCode: string;
country: string;
imageFilename: string;
itemRequestNotificationRadius: number | null;
}>({
firstName: "",
lastName: "",
email: "",
phone: "",
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "",
imageFilename: "",
itemRequestNotificationRadius: 10,
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [stats, setStats] = useState({
itemsListed: 0,
acceptedRentals: 0,
totalRentals: 0,
});
const [showPersonalInfo, setShowPersonalInfo] = useState(false);
const [availabilityData, setAvailabilityData] = useState({
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 [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",
latitude: undefined as number | undefined,
longitude: undefined as number | undefined,
});
const [addressGeocoding, setAddressGeocoding] = useState(false);
const [addressGeocodeError, setAddressGeocodeError] = useState<string | null>(
null
);
const [addressGeocodeSuccess, setAddressGeocodeSuccess] = useState(false);
// Rental history state
const [pastRenterRentals, setPastRenterRentals] = useState<Rental[]>([]);
const [pastOwnerRentals, setPastOwnerRentals] = useState<Rental[]>([]);
const [rentalHistoryLoading, setRentalHistoryLoading] = useState(true);
const [showReviewModal, setShowReviewModal] = useState(false);
const [showReviewRenterModal, setShowReviewRenterModal] = useState(false);
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
const [selectedRentalForReview, setSelectedRentalForReview] =
useState<Rental | null>(null);
const [showReviewDetailsModal, setShowReviewDetailsModal] = useState(false);
const [selectedRentalForDetails, setSelectedRentalForDetails] =
useState<Rental | null>(null);
const [reviewDetailsUserType, setReviewDetailsUserType] = useState<
"renter" | "owner"
>("renter");
useEffect(() => {
fetchProfile();
fetchStats();
fetchUserAddresses();
fetchUserAvailability();
fetchRentalHistory();
}, []);
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();
setProfileData(response.data);
setFormData({
firstName: response.data.firstName || "",
lastName: response.data.lastName || "",
email: response.data.email || "",
phone: response.data.phone || "",
address1: response.data.address1 || "",
address2: response.data.address2 || "",
city: response.data.city || "",
state: response.data.state || "",
zipCode: response.data.zipCode || "",
country: response.data.country || "",
imageFilename: response.data.imageFilename || "",
itemRequestNotificationRadius:
response.data.itemRequestNotificationRadius || 10,
});
if (response.data.imageFilename) {
setImagePreview(getPublicImageUrl(response.data.imageFilename));
}
} catch (err: any) {
setError(err.response?.data?.message || "Failed to fetch profile");
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
try {
// Fetch user's items
const itemsResponse = await itemAPI.getItems();
const allItems = itemsResponse.data.items || itemsResponse.data || [];
const myItems = allItems.filter(
(item: Item) => item.ownerId === user?.id
);
// Fetch rentals where user is the owner (rentals on user's items)
const ownerRentalsResponse = await rentalAPI.getListings();
const ownerRentals: Rental[] = ownerRentalsResponse.data;
const acceptedRentals = ownerRentals.filter((r) =>
["confirmed", "active"].includes(r.status)
);
setStats({
itemsListed: myItems.length,
acceptedRentals: acceptedRentals.length,
totalRentals: ownerRentals.length,
});
} catch (err) {
console.error("Failed to fetch stats:", err);
}
};
// Helper functions for rental history
const formatTime = (timeString?: string) => {
if (!timeString || timeString.trim() === "") return "";
try {
const [hour, minute] = timeString.split(":");
const hourNum = parseInt(hour);
const hour12 = hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
const period = hourNum < 12 ? "AM" : "PM";
return `${hour12}:${minute} ${period}`;
} catch (error) {
return "";
}
};
const formatDateTime = (dateTimeString: string) => {
const date = new Date(dateTimeString).toLocaleDateString();
return date;
};
const fetchRentalHistory = async () => {
try {
// Fetch past rentals as a renter
const renterResponse = await rentalAPI.getRentals();
const pastRenterRentals = renterResponse.data.filter((r: Rental) =>
["completed", "cancelled", "declined"].includes(r.status)
);
setPastRenterRentals(pastRenterRentals);
// Fetch past rentals as an owner
const ownerResponse = await rentalAPI.getListings();
const pastOwnerRentals = ownerResponse.data.filter((r: Rental) =>
["completed", "cancelled", "declined"].includes(r.status)
);
setPastOwnerRentals(pastOwnerRentals);
} catch (err) {
console.error("Failed to fetch rental history:", err);
} finally {
setRentalHistoryLoading(false);
}
};
const handleReviewClick = (rental: Rental) => {
setSelectedRental(rental);
setShowReviewModal(true);
};
const handleReviewSuccess = () => {
fetchRentalHistory(); // Refresh to show updated review status
alert("Thank you for your review!");
};
const handleCompleteClick = async (rental: Rental) => {
try {
await rentalAPI.markAsCompleted(rental.id);
setSelectedRentalForReview(rental);
setShowReviewRenterModal(true);
fetchRentalHistory(); // Refresh rental history
} catch (err: any) {
alert(
"Failed to mark rental as completed: " +
(err.response?.data?.error || err.message)
);
}
};
const handleReviewRenterSuccess = () => {
fetchRentalHistory(); // Refresh to show updated review status
};
const handleViewReviewDetails = (
rental: Rental,
userType: "renter" | "owner"
) => {
setSelectedRentalForDetails(rental);
setReviewDetailsUserType(userType);
setShowReviewDetailsModal(true);
};
const handleCloseReviewDetails = () => {
setShowReviewDetailsModal(false);
setSelectedRentalForDetails(null);
};
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImageFile(file);
// Show preview
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
// Upload image to S3
try {
const { key, publicUrl } = await uploadFile("profile", file);
// Update the imageFilename in formData with the S3 key
setFormData((prev) => ({
...prev,
imageFilename: key,
}));
// Update preview to use the S3 URL
setImagePreview(publicUrl);
} catch (err: any) {
console.error("Image upload error:", err);
setError(err.message || "Failed to upload image");
// Reset on error
setImageFile(null);
setImagePreview(
profileData?.imageFilename
? getPublicImageUrl(profileData.imageFilename)
: null
);
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(null);
try {
// Don't send imageFilename in the update data as it's handled separately
const { imageFilename, ...updateData } = formData;
const response = await userAPI.updateProfile(updateData);
setProfileData(response.data);
updateUser(response.data); // Update the auth context
setEditing(false);
} catch (err: any) {
console.error("Profile update error:", err.response?.data);
const errorMessage =
err.response?.data?.error ||
err.response?.data?.message ||
"Failed to update profile";
const errorDetails = err.response?.data?.details;
if (errorDetails && Array.isArray(errorDetails)) {
const detailMessages = errorDetails
.map((d: any) => `${d.field}: ${d.message}`)
.join(", ");
setError(`${errorMessage} - ${detailMessages}`);
} else {
setError(errorMessage);
}
}
};
const handleCancel = () => {
setEditing(false);
setError(null);
setSuccess(null);
// Reset form to original data
if (profileData) {
setFormData({
firstName: profileData.firstName || "",
lastName: profileData.lastName || "",
email: profileData.email || "",
phone: profileData.phone || "",
address1: profileData.address1 || "",
address2: profileData.address2 || "",
city: profileData.city || "",
state: profileData.state || "",
zipCode: profileData.zipCode || "",
country: profileData.country || "",
imageFilename: profileData.imageFilename || "",
itemRequestNotificationRadius:
profileData.itemRequestNotificationRadius || 10,
});
setImagePreview(
profileData.imageFilename ? getPublicImageUrl(profileData.imageFilename) : null
);
}
};
const handleAvailabilityChange = (field: string, value: string | boolean) => {
setAvailabilityData((prev) => ({ ...prev, [field]: value }));
};
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,
},
},
}));
};
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 handleSaveNotificationPreferences = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(null);
try {
const response = await userAPI.updateProfile({
itemRequestNotificationRadius: formData.itemRequestNotificationRadius,
});
setProfileData(response.data);
updateUser(response.data);
setSuccess("Notification preferences saved successfully");
setTimeout(() => setSuccess(null), 3000);
} catch (err: any) {
console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage =
err.response?.data?.error ||
err.response?.data?.message ||
"Failed to update notification preferences";
setError(errorMessage);
}
};
const handleNotificationRadiusChange = async (
e: React.ChangeEvent<HTMLSelectElement>
) => {
const { value } = e.target;
setFormData((prev) => ({
...prev,
itemRequestNotificationRadius: parseInt(value),
}));
setError(null);
try {
const response = await userAPI.updateProfile({
itemRequestNotificationRadius: parseInt(value),
});
setProfileData(response.data);
updateUser(response.data);
} catch (err: any) {
console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage =
err.response?.data?.error ||
err.response?.data?.message ||
"Failed to update notification radius";
setError(errorMessage);
}
};
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",
latitude: undefined,
longitude: undefined,
});
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,
latitude: address.latitude,
longitude: address.longitude,
});
setEditingAddressId(address.id);
setShowAddressForm(true);
};
const handleAddressFormChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setAddressFormData((prev) => ({ ...prev, [name]: value }));
};
// Geocoding function for address form
const geocodeAddressForm = useCallback(
async (addressData: typeof addressFormData) => {
if (
!geocodingService.isAddressComplete(addressData as AddressComponents)
) {
return null;
}
setAddressGeocoding(true);
setAddressGeocodeError(null);
setAddressGeocodeSuccess(false);
try {
const result = await geocodingService.geocodeAddress(
addressData as AddressComponents
);
if ("error" in result) {
setAddressGeocodeError(result.details || result.error);
return null;
} else {
setAddressGeocodeSuccess(true);
setAddressFormData((prev) => ({
...prev,
latitude: result.latitude,
longitude: result.longitude,
}));
// Clear success message after 3 seconds
setTimeout(() => setAddressGeocodeSuccess(false), 3000);
return { latitude: result.latitude, longitude: result.longitude };
}
} catch (error) {
setAddressGeocodeError("Failed to geocode address");
return null;
} finally {
setAddressGeocoding(false);
}
},
[]
);
const handleSaveAddress = async (e?: React.FormEvent | React.MouseEvent) => {
e?.preventDefault();
// Try to geocode the address before saving
let coordinates = null;
try {
coordinates = await geocodeAddressForm(addressFormData);
} catch (error) {
// Geocoding failed, but we'll continue with saving
console.warn(
"Geocoding failed, saving address without coordinates:",
error
);
}
// Prepare the data to save, including coordinates if geocoding succeeded
const dataToSave = {
...addressFormData,
...(coordinates && {
latitude: coordinates.latitude,
longitude: coordinates.longitude,
}),
};
try {
if (editingAddressId) {
// Update existing address
const response = await addressAPI.updateAddress(
editingAddressId,
dataToSave
);
setUserAddresses((prev) =>
prev.map((addr) =>
addr.id === editingAddressId ? response.data : addr
)
);
} else {
// Create new address
const response = await addressAPI.createAddress({
...dataToSave,
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",
latitude: undefined,
longitude: undefined,
});
setAddressGeocoding(false);
setAddressGeocodeError(null);
setAddressGeocodeSuccess(false);
};
// Use address autocomplete hook
const { parsePlace } = useAddressAutocomplete();
// Handle place selection from autocomplete
const handlePlaceSelect = useCallback(
(place: PlaceDetails) => {
const parsedAddress = parsePlace(place);
if (parsedAddress) {
setAddressFormData((prev) => ({
...prev,
...parsedAddress,
}));
setAddressGeocodeSuccess(true);
setTimeout(() => setAddressGeocodeSuccess(false), 3000);
}
},
[parsePlace]
);
if (loading) {
return (
<div className="container mt-5">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
return (
<div className="container mt-4 mb-5">
<h1 className="mb-4">Profile</h1>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
{success && (
<div className="alert alert-success" role="alert">
{success}
</div>
)}
<div className="row">
{/* Left Sidebar Menu */}
<div className="col-md-3">
<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")}
>
<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")}
>
<i className="bi bi-gear me-2"></i>
Owner Settings
</button>
<button
className={`list-group-item list-group-item-action ${
activeSection === "notification-preferences" ? "active" : ""
}`}
onClick={() => setActiveSection("notification-preferences")}
>
<i className="bi bi-bell me-2"></i>
Notification Preferences
</button>
<button
className={`list-group-item list-group-item-action ${
activeSection === "rental-history" ? "active" : ""
}`}
onClick={() => setActiveSection("rental-history")}
>
<i className="bi bi-clock-history me-2"></i>
Rental History
</button>
<button
className="list-group-item list-group-item-action text-danger"
onClick={logout}
>
<i className="bi bi-box-arrow-right me-2"></i>
Log Out
</button>
</div>
</div>
</div>
{/* Right Content Area */}
<div className="col-md-9">
{/* Overview Section */}
{activeSection === "overview" && (
<div>
<h4 className="mb-4">Overview</h4>
{/* Profile Card */}
<div className="card mb-4">
<div className="card-body">
<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="imageFilenameOverview"
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="imageFilenameOverview"
accept="image/*"
onChange={handleImageChange}
className="d-none"
/>
</label>
)}
</div>
<div>
<h5>
{profileData?.firstName} {profileData?.lastName}
</h5>
<p className="text-muted">@{profileData?.username}</p>
</div>
</div>
</div>
</div>
{/* Personal Information Card */}
<div className="card mb-4">
<div className="card-body">
<div className="d-flex align-items-center justify-content-between mb-3">
<div className="d-flex align-items-center">
<h5 className="card-title mb-0">Personal Information</h5>
<button
type="button"
className="btn btn-link text-primary p-0 ms-2"
onClick={() => setShowPersonalInfo(!showPersonalInfo)}
style={{ textDecoration: "none" }}
>
<i
className={`bi ${
showPersonalInfo ? "bi-eye" : "bi-eye-slash"
} fs-5`}
></i>
</button>
</div>
{showPersonalInfo && (
<div>
{editing ? (
<div className="d-flex gap-2">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={handleSubmit}
>
Save Changes
</button>
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={handleCancel}
>
Cancel
</button>
</div>
) : (
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => setEditing(true)}
>
Edit Information
</button>
)}
</div>
)}
</div>
{showPersonalInfo && (
<form onSubmit={handleSubmit}>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="firstName" className="form-label">
First Name
</label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
disabled={!editing}
required
/>
</div>
<div className="col-md-6">
<label htmlFor="lastName" className="form-label">
Last Name
</label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
disabled={!editing}
required
/>
</div>
</div>
<div className="mb-3">
<label htmlFor="email" className="form-label">
Email
</label>
<input
type="email"
className="form-control"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
disabled={!editing}
/>
</div>
<div className="mb-3">
<label htmlFor="phone" className="form-label">
Phone Number
</label>
<input
type="tel"
className="form-control"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="(123) 456-7890"
disabled={!editing}
/>
</div>
<hr className="my-4" />
{/* Saved Addresses Section */}
<div className="mb-3">
<label className="form-label">Saved Addresses</label>
{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 mb-2">
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 && (
<div>
<div className="row mb-3">
<div className="col-md-6">
<label
htmlFor="addressFormAddress1"
className="form-label"
>
Address Line 1 *
</label>
<AddressAutocomplete
id="addressFormAddress1"
name="address1"
value={addressFormData.address1}
onChange={(value) => {
const syntheticEvent = {
target: {
name: "address1",
value,
type: "text",
},
} as React.ChangeEvent<HTMLInputElement>;
handleAddressFormChange(syntheticEvent);
}}
onPlaceSelect={handlePlaceSelect}
placeholder="Start typing an address..."
className="form-control"
required
countryRestriction="us"
types={["address"]}
/>
</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}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSaveAddress(e);
}
}}
placeholder="12345"
required
/>
</div>
</div>
<div className="d-flex gap-2">
<button
type="button"
className="btn btn-primary"
onClick={handleSaveAddress}
>
{editingAddressId
? "Update Address"
: "Save Address"}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancelAddressForm}
>
Cancel
</button>
</div>
</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>
)}
{/* Rental History Section */}
{activeSection === "rental-history" && (
<div>
<h4 className="mb-4">Rental History</h4>
{rentalHistoryLoading ? (
<div className="text-center py-5">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<>
{/* As Renter Section */}
{pastRenterRentals.length > 0 && (
<div className="mb-5">
<h5 className="mb-3">
<i className="bi bi-person me-2"></i>
As Renter ({pastRenterRentals.length})
</h5>
<div className="row">
{pastRenterRentals.map((rental) => (
<div
key={rental.id}
className="col-md-6 col-lg-4 mb-4"
>
<div className="card h-100">
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{
height: "150px",
objectFit: "cover",
}}
/>
)}
<div className="card-body">
<h6 className="card-title">
{rental.item
? rental.item.name
: "Item Unavailable"}
</h6>
<div className="mb-2">
<span
className={`badge ${
rental.status === "completed"
? "bg-success"
: rental.status === "declined"
? "bg-secondary"
: "bg-danger"
}`}
>
{rental.status.charAt(0).toUpperCase() +
rental.status.slice(1)}
</span>
</div>
<p className="mb-1 small">
<strong>Period:</strong>
<br />
<strong>Start:</strong>{" "}
{formatDateTime(rental.startDateTime)}
<br />
<strong>End:</strong>{" "}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 small">
<strong>Total:</strong> ${rental.totalAmount}
</p>
{rental.owner && (
<p className="mb-1 small">
<strong>Owner:</strong>{" "}
<span
onClick={() =>
navigate(`/users/${rental.ownerId}`)
}
style={{ cursor: "pointer" }}
>
{rental.owner.firstName}{" "}
{rental.owner.lastName}
</span>
</p>
)}
{rental.status === "declined" &&
rental.declineReason && (
<div className="alert alert-warning mt-2 mb-1 p-2 small">
<strong>Decline reason:</strong>{" "}
{rental.declineReason}
</div>
)}
<div className="d-flex gap-2 mt-3">
{rental.status === "completed" &&
!rental.itemRating &&
!rental.itemReviewSubmittedAt && (
<button
className="btn btn-sm btn-primary"
onClick={() =>
handleReviewClick(rental)
}
>
Review
</button>
)}
{rental.itemReviewSubmittedAt &&
!rental.itemReviewVisible && (
<div className="text-info small">
<i className="bi bi-clock me-1"></i>
Review Submitted
</div>
)}
{((rental.renterPrivateMessage &&
rental.renterReviewVisible) ||
(rental.itemReviewVisible &&
rental.itemRating)) && (
<button
className="btn btn-sm btn-outline-primary mt-2"
onClick={() =>
handleViewReviewDetails(
rental,
"renter"
)
}
>
View Review Details
</button>
)}
{rental.status === "completed" &&
rental.rating &&
!rental.itemRating && (
<div className="text-success small">
<i className="bi bi-check-circle-fill me-1"></i>
Reviewed ({rental.rating}/5)
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* As Owner Section */}
{pastOwnerRentals.length > 0 && (
<div className="mb-5">
<h5 className="mb-3">
<i className="bi bi-house me-2"></i>
As Owner ({pastOwnerRentals.length})
</h5>
<div className="row">
{pastOwnerRentals.map((rental) => (
<div
key={rental.id}
className="col-md-6 col-lg-4 mb-4"
>
<div className="card h-100">
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{
height: "150px",
objectFit: "cover",
}}
/>
)}
<div className="card-body">
<h6 className="card-title">
{rental.item
? rental.item.name
: "Item Unavailable"}
</h6>
{rental.renter && (
<p className="mb-1 small">
<strong>Renter:</strong>{" "}
<span
onClick={() =>
navigate(`/users/${rental.renterId}`)
}
style={{ cursor: "pointer" }}
>
{rental.renter.firstName}{" "}
{rental.renter.lastName}
</span>
</p>
)}
<div className="mb-2">
<span
className={`badge ${
rental.status === "completed"
? "bg-success"
: rental.status === "declined"
? "bg-secondary"
: "bg-danger"
}`}
>
{rental.status.charAt(0).toUpperCase() +
rental.status.slice(1)}
</span>
</div>
<p className="mb-1 small">
<strong>Period:</strong>
<br />
{formatDateTime(rental.startDateTime)} -{" "}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 small">
<strong>Total:</strong> ${rental.totalAmount}
</p>
<div className="d-flex gap-2 mt-3">
{rental.status === "completed" &&
!rental.renterRating &&
!rental.renterReviewSubmittedAt && (
<button
className="btn btn-sm btn-primary"
onClick={() => {
setSelectedRentalForReview(rental);
setShowReviewRenterModal(true);
}}
>
Review Renter
</button>
)}
{rental.renterReviewSubmittedAt &&
!rental.renterReviewVisible && (
<div className="text-info small">
<i className="bi bi-clock me-1"></i>
Review Submitted
</div>
)}
{((rental.itemPrivateMessage &&
rental.itemReviewVisible) ||
(rental.renterReviewVisible &&
rental.renterRating)) && (
<button
className="btn btn-sm btn-outline-primary mt-2"
onClick={() =>
handleViewReviewDetails(rental, "owner")
}
>
View Review Details
</button>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{pastRenterRentals.length === 0 &&
pastOwnerRentals.length === 0 && (
<div className="text-center py-5">
<i
className="bi bi-clock-history text-muted mb-3"
style={{ fontSize: "3rem" }}
></i>
<h5 className="text-muted">No Rental History</h5>
<p className="text-muted">
Your completed rentals and rental requests will appear
here.
</p>
</div>
)}
</>
)}
</div>
)}
{/* Owner Settings Section */}
{activeSection === "owner-settings" && (
<div>
<h4 className="mb-4">Owner Settings</h4>
{/* Availability Card */}
<div className="card">
<div className="card-body">
<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>
)}
{/* Notification Preferences Section */}
{activeSection === "notification-preferences" && (
<div>
<h4 className="mb-4">Notification Preferences</h4>
<div className="card">
<div className="card-body">
<div className="mb-3">
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
id="enableItemRequestNotifications"
checked={
formData.itemRequestNotificationRadius !== null &&
formData.itemRequestNotificationRadius !== undefined
}
onChange={async (e) => {
const isEnabled = e.target.checked;
const newRadius = isEnabled ? 10 : null; // Default to 10 miles when enabled
setFormData((prev) => ({
...prev,
itemRequestNotificationRadius: newRadius,
}));
setError(null);
try {
const response = await userAPI.updateProfile({
itemRequestNotificationRadius: newRadius,
});
setProfileData(response.data);
updateUser(response.data);
} catch (err: any) {
console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage =
err.response?.data?.error ||
err.response?.data?.message ||
"Failed to update notification preferences";
setError(errorMessage);
}
}}
/>
<label
className="form-check-label"
htmlFor="enableItemRequestNotifications"
>
Enable Item Request Notifications
</label>
</div>
<div className="form-text mb-3">
Receive notifications when someone nearby posts an item
request
</div>
</div>
{formData.itemRequestNotificationRadius !== null &&
formData.itemRequestNotificationRadius !== undefined && (
<div className="mb-3">
<label
htmlFor="itemRequestNotificationRadius"
className="form-label"
>
Notification Radius
</label>
<select
className="form-select"
id="itemRequestNotificationRadius"
name="itemRequestNotificationRadius"
value={formData.itemRequestNotificationRadius}
onChange={handleNotificationRadiusChange}
>
<option value="5">5 miles</option>
<option value="10">10 miles</option>
<option value="25">25 miles</option>
<option value="50">50 miles</option>
<option value="100">100 miles</option>
</select>
<div className="form-text">
You'll receive notifications for item requests within
this distance from your primary address
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
{/* Review Modals */}
{selectedRental && (
<ReviewItemModal
show={showReviewModal}
onClose={() => {
setShowReviewModal(false);
setSelectedRental(null);
}}
rental={selectedRental}
onSuccess={handleReviewSuccess}
/>
)}
{selectedRentalForReview && (
<ReviewRenterModal
show={showReviewRenterModal}
onClose={() => {
setShowReviewRenterModal(false);
setSelectedRentalForReview(null);
}}
rental={selectedRentalForReview}
onSuccess={handleReviewRenterSuccess}
/>
)}
{/* Review Details Modal */}
{selectedRentalForDetails && (
<ReviewDetailsModal
show={showReviewDetailsModal}
onClose={handleCloseReviewDetails}
rental={selectedRentalForDetails}
userType={reviewDetailsUserType}
/>
)}
</div>
);
};
export default Profile;