import React, { useState, useEffect, useCallback } from "react"; import { useAuth } from "../contexts/AuthContext"; 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"; import ReviewItemModal from "../components/ReviewModal"; import ReviewRenterModal from "../components/ReviewRenterModal"; import ReviewDetailsModal from "../components/ReviewDetailsModal"; import { geocodingService, AddressComponents, } from "../services/geocodingService"; const Profile: React.FC = () => { const { user, updateUser, logout } = useAuth(); 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: "", lastName: "", email: "", phone: "", address1: "", address2: "", city: "", state: "", zipCode: "", country: "", profileImage: "", }); const [imageFile, setImageFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [stats, setStats] = useState({ itemsListed: 0, acceptedRentals: 0, totalRentals: 0, }); 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 || "", profileImage: response.data.profileImage || "", }); if (response.data.profileImage) { setImagePreview(getImageUrl(response.data.profileImage)); } } 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.getMyListings(); 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.getMyRentals(); const pastRenterRentals = renterResponse.data.filter((r: Rental) => ["completed", "cancelled"].includes(r.status) ); setPastRenterRentals(pastRenterRentals); // Fetch past rentals as an owner const ownerResponse = await rentalAPI.getMyListings(); const pastOwnerRentals = ownerResponse.data.filter((r: Rental) => ["completed", "cancelled"].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 ) => { 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 immediately try { const formData = new FormData(); formData.append("profileImage", file); const response = await userAPI.uploadProfileImage(formData); // Update the profileImage in formData with the new filename setFormData((prev) => ({ ...prev, profileImage: response.data.filename, })); // Update preview to use the uploaded image URL setImagePreview(getImageUrl(response.data.imageUrl)); } catch (err: any) { console.error("Image upload error:", err); setError(err.response?.data?.error || "Failed to upload image"); // Reset on error setImageFile(null); setImagePreview( profileData?.profileImage ? getImageUrl(profileData.profileImage) : null ); } } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); try { // Don't send profileImage in the update data as it's handled separately const { profileImage, ...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 || "", profileImage: profileData.profileImage || "", }); setImagePreview( profileData.profileImage ? getImageUrl(profileData.profileImage) : 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 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; } setAddressGeocoding(true); setAddressGeocodeError(null); setAddressGeocodeSuccess(false); try { const result = await geocodingService.geocodeAddress( addressData as AddressComponents ); if ("error" in result) { setAddressGeocodeError(result.details || result.error); } else { setAddressGeocodeSuccess(true); setAddressFormData((prev) => ({ ...prev, latitude: result.latitude, longitude: result.longitude, })); // Clear success message after 3 seconds setTimeout(() => setAddressGeocodeSuccess(false), 3000); } } catch (error) { setAddressGeocodeError("Failed to geocode address"); } finally { setAddressGeocoding(false); } }, [] ); const handleSaveAddress = async (e: React.FormEvent) => { e.preventDefault(); // Try to geocode the address before saving try { await geocodeAddressForm(addressFormData); } catch (error) { // Geocoding failed, but we'll continue with saving console.warn( "Geocoding failed, saving address without coordinates:", error ); } 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", latitude: undefined, longitude: undefined, }); setAddressGeocoding(false); setAddressGeocodeError(null); setAddressGeocodeSuccess(false); }; 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 (
Loading...
); } return (

Profile

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

Overview

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

@{profileData?.username}

{profileData?.isVerified && ( {" "} Verified )}
)}
{/* 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?.images && rental.item.images[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:{" "} {rental.owner.firstName}{" "} {rental.owner.lastName}

)} {rental.status === "cancelled" && rental.rejectionReason && (
Rejection reason:{" "} {rental.rejectionReason}
)}
{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?.images && rental.item.images[0] && ( {rental.item.name} )}
{rental.item ? rental.item.name : "Item Unavailable"}
{rental.renter && (

Renter:{" "} {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.

)} )}
)} {/* Personal Information Section */} {activeSection === "personal-info" && (

Personal Information

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

Owner Settings

{/* Addresses Card */}
Saved Addresses
{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 && (
)} )}
{/* Availability Card */}
Availability
)} {/* Placeholder sections for other menu items */} {activeSection === "notifications" && (

Notification Settings

Notification preferences coming soon...

)} {activeSection === "privacy" && (

Privacy & Security

Privacy and security settings coming soon...

)}
{/* 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;