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(null); const [success, setSuccess] = useState(null); const [activeSection, setActiveSection] = useState("overview"); const [profileData, setProfileData] = useState(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(null); const [imagePreview, setImagePreview] = useState(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([]); const [addressesLoading, setAddressesLoading] = useState(true); const [showAddressForm, setShowAddressForm] = useState(false); const [editingAddressId, setEditingAddressId] = useState(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( null ); const [addressGeocodeSuccess, setAddressGeocodeSuccess] = useState(false); // Rental history state const [pastRenterRentals, setPastRenterRentals] = useState([]); const [pastOwnerRentals, setPastOwnerRentals] = useState([]); const [rentalHistoryLoading, setRentalHistoryLoading] = useState(true); const [showReviewModal, setShowReviewModal] = useState(false); const [showReviewRenterModal, setShowReviewRenterModal] = useState(false); const [selectedRental, setSelectedRental] = useState(null); const [selectedRentalForReview, setSelectedRentalForReview] = useState(null); const [showReviewDetailsModal, setShowReviewDetailsModal] = useState(false); const [selectedRentalForDetails, setSelectedRentalForDetails] = useState(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) => { 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 ) => { 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 ) => { 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 (
Loading...
); } return (

Profile

{error && (
{error}
)} {success && (
{success}
)}
{/* Left Sidebar Menu */}
{/* Right Content Area */}
{/* Overview Section */} {activeSection === "overview" && (

Overview

{/* Profile Card */}
{imagePreview ? ( Profile ) : (
)} {editing && ( )}
{profileData?.firstName} {profileData?.lastName}

@{profileData?.username}

{/* Personal Information Card */}
Personal Information
{showPersonalInfo && (
{editing ? (
) : ( )}
)}
{showPersonalInfo && (

{/* Saved Addresses Section */}
{addressesLoading ? (
Loading addresses...
) : ( <> {userAddresses.length === 0 && !showAddressForm ? (

No saved addresses yet

Add an address or create your first listing to save one automatically
) : ( <> {userAddresses.length > 0 && !showAddressForm && ( <>
{userAddresses.map((address) => (
{formatAddressDisplay(address)}
{address.address2 && ( {address.address2} )}
))}
)} )} {/* Show Add New Address button even when no addresses exist */} {userAddresses.length === 0 && !showAddressForm && (
)} {/* Address Form */} {showAddressForm && (
{ const syntheticEvent = { target: { name: "address1", value, type: "text", }, } as React.ChangeEvent; handleAddressFormChange(syntheticEvent); }} onPlaceSelect={handlePlaceSelect} placeholder="Start typing an address..." className="form-control" required countryRestriction="us" types={["address"]} />
{ if (e.key === "Enter") { handleSaveAddress(e); } }} placeholder="12345" required />
)} )}
)}
{/* Stats Card */}
Account Statistics

{stats.itemsListed}

Items Listed

{stats.acceptedRentals}

Active Rentals

{stats.totalRentals}

Total Rentals
)} {/* Rental History Section */} {activeSection === "rental-history" && (

Rental History

{rentalHistoryLoading ? (
Loading...
) : ( <> {/* As Renter Section */} {pastRenterRentals.length > 0 && (
As Renter ({pastRenterRentals.length})
{pastRenterRentals.map((rental) => (
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} )}
{rental.item ? rental.item.name : "Item Unavailable"}
{rental.status.charAt(0).toUpperCase() + rental.status.slice(1)}

Period:
Start:{" "} {formatDateTime(rental.startDateTime)}
End:{" "} {formatDateTime(rental.endDateTime)}

Total: ${rental.totalAmount}

{rental.owner && (

Owner:{" "} navigate(`/users/${rental.ownerId}`) } style={{ cursor: "pointer" }} > {rental.owner.firstName}{" "} {rental.owner.lastName}

)} {rental.status === "declined" && rental.declineReason && (
Decline reason:{" "} {rental.declineReason}
)}
{rental.status === "completed" && !rental.itemRating && !rental.itemReviewSubmittedAt && ( )} {rental.itemReviewSubmittedAt && !rental.itemReviewVisible && (
Review Submitted
)} {((rental.renterPrivateMessage && rental.renterReviewVisible) || (rental.itemReviewVisible && rental.itemRating)) && ( )} {rental.status === "completed" && rental.rating && !rental.itemRating && (
Reviewed ({rental.rating}/5)
)}
))}
)} {/* As Owner Section */} {pastOwnerRentals.length > 0 && (
As Owner ({pastOwnerRentals.length})
{pastOwnerRentals.map((rental) => (
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} )}
{rental.item ? rental.item.name : "Item Unavailable"}
{rental.renter && (

Renter:{" "} navigate(`/users/${rental.renterId}`) } style={{ cursor: "pointer" }} > {rental.renter.firstName}{" "} {rental.renter.lastName}

)}
{rental.status.charAt(0).toUpperCase() + rental.status.slice(1)}

Period:
{formatDateTime(rental.startDateTime)} -{" "} {formatDateTime(rental.endDateTime)}

Total: ${rental.totalAmount}

{rental.status === "completed" && !rental.renterRating && !rental.renterReviewSubmittedAt && ( )} {rental.renterReviewSubmittedAt && !rental.renterReviewVisible && (
Review Submitted
)} {((rental.itemPrivateMessage && rental.itemReviewVisible) || (rental.renterReviewVisible && rental.renterRating)) && ( )}
))}
)} {/* Empty State */} {pastRenterRentals.length === 0 && pastOwnerRentals.length === 0 && (
No Rental History

Your completed rentals and rental requests will appear here.

)} )}
)} {/* Owner Settings Section */} {activeSection === "owner-settings" && (

Owner Settings

{/* Availability Card */}
Availability
)} {/* Notification Preferences Section */} {activeSection === "notification-preferences" && (

Notification Preferences

{ 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); } }} />
Receive notifications when someone nearby posts an item request
{formData.itemRequestNotificationRadius !== null && formData.itemRequestNotificationRadius !== undefined && (
You'll receive notifications for item requests within this distance from your primary address
)}
)}
{/* Review Modals */} {selectedRental && ( { setShowReviewModal(false); setSelectedRental(null); }} rental={selectedRental} onSuccess={handleReviewSuccess} /> )} {selectedRentalForReview && ( { setShowReviewRenterModal(false); setSelectedRentalForReview(null); }} rental={selectedRentalForReview} onSuccess={handleReviewRenterSuccess} /> )} {/* Review Details Modal */} {selectedRentalForDetails && ( )}
); }; export default Profile;