1503 lines
56 KiB
TypeScript
1503 lines
56 KiB
TypeScript
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<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: "",
|
|
lastName: "",
|
|
email: "",
|
|
phone: "",
|
|
address1: "",
|
|
address2: "",
|
|
city: "",
|
|
state: "",
|
|
zipCode: "",
|
|
country: "",
|
|
profileImage: "",
|
|
});
|
|
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 [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 || "",
|
|
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<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
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 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<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;
|
|
}
|
|
|
|
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 (
|
|
<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">
|
|
<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 === "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 ${
|
|
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")}
|
|
>
|
|
<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")}
|
|
>
|
|
<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")}
|
|
>
|
|
<i className="bi bi-credit-card me-2"></i>
|
|
Payment Methods
|
|
</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">
|
|
<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>
|
|
<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>
|
|
</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?.images && rental.item.images[0] && (
|
|
<img
|
|
src={rental.item.images[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"
|
|
: "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>{" "}
|
|
{rental.owner.firstName}{" "}
|
|
{rental.owner.lastName}
|
|
</p>
|
|
)}
|
|
|
|
{rental.status === "cancelled" &&
|
|
rental.rejectionReason && (
|
|
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
|
<strong>Rejection reason:</strong>{" "}
|
|
{rental.rejectionReason}
|
|
</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?.images && rental.item.images[0] && (
|
|
<img
|
|
src={rental.item.images[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>{" "}
|
|
{rental.renter.firstName}{" "}
|
|
{rental.renter.lastName}
|
|
</p>
|
|
)}
|
|
|
|
<div className="mb-2">
|
|
<span
|
|
className={`badge ${
|
|
rental.status === "completed"
|
|
? "bg-success"
|
|
: "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>
|
|
)}
|
|
|
|
{/* Personal Information Section */}
|
|
{activeSection === "personal-info" && (
|
|
<div>
|
|
<h4 className="mb-4">Personal Information</h4>
|
|
<div className="card">
|
|
<div className="card-body">
|
|
<form onSubmit={handleSubmit}>
|
|
<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>
|
|
|
|
{editing ? (
|
|
<div className="d-flex gap-2">
|
|
<button type="submit" className="btn btn-primary">
|
|
Save Changes
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
onClick={handleCancel}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
onClick={() => setEditing(true)}
|
|
>
|
|
Edit Information
|
|
</button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Owner Settings Section */}
|
|
{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">
|
|
<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" && (
|
|
<div>
|
|
<h4 className="mb-4">Notification Settings</h4>
|
|
<div className="card">
|
|
<div className="card-body">
|
|
<p className="text-muted">
|
|
Notification preferences coming soon...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{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>
|
|
</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;
|