Files
rentall-app/frontend/src/pages/Profile.tsx
2025-07-30 19:12:56 -04:00

490 lines
18 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { userAPI, itemAPI, rentalAPI } from '../services/api';
import { User, Item, Rental } from '../types';
import { getImageUrl } from '../utils/imageUrl';
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 [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
});
useEffect(() => {
fetchProfile();
fetchStats();
}, []);
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);
}
};
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
);
}
};
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">
<div className="row justify-content-center">
<div className="col-md-8">
<h1 className="mb-4">My Profile</h1>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
{success && (
<div className="alert alert-success" role="alert">
{success}
</div>
)}
<div className="card">
<div className="card-body">
<form onSubmit={handleSubmit}>
<div className="text-center mb-4">
<div className="position-relative d-inline-block">
{imagePreview ? (
<img
src={imagePreview}
alt="Profile"
className="rounded-circle"
style={{ width: '150px', height: '150px', objectFit: 'cover' }}
/>
) : (
<div
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
style={{ width: '150px', height: '150px' }}
>
<i className="bi bi-person-fill text-white" style={{ fontSize: '3rem' }}></i>
</div>
)}
{editing && (
<label
htmlFor="profileImage"
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
style={{ width: '40px', height: '40px', padding: '0' }}
>
<i className="bi bi-camera-fill"></i>
<input
type="file"
id="profileImage"
accept="image/*"
onChange={handleImageChange}
className="d-none"
/>
</label>
)}
</div>
<h5 className="mt-3">{profileData?.firstName} {profileData?.lastName}</h5>
<p className="text-muted">@{profileData?.username}</p>
{profileData?.isVerified && (
<span className="badge bg-success">
<i className="bi bi-check-circle-fill"></i> Verified
</span>
)}
</div>
<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="+1 (555) 123-4567"
disabled={!editing}
/>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address1" className="form-label">Address Line 1</label>
<input
type="text"
className="form-control"
id="address1"
name="address1"
value={formData.address1}
onChange={handleChange}
placeholder="123 Main Street"
disabled={!editing}
/>
</div>
<div className="col-md-6">
<label htmlFor="address2" className="form-label">Address Line 2</label>
<input
type="text"
className="form-control"
id="address2"
name="address2"
value={formData.address2}
onChange={handleChange}
placeholder="Apt, Suite, Unit, etc."
disabled={!editing}
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="city" className="form-label">City</label>
<input
type="text"
className="form-control"
id="city"
name="city"
value={formData.city}
onChange={handleChange}
disabled={!editing}
/>
</div>
<div className="col-md-3">
<label htmlFor="state" className="form-label">State</label>
<input
type="text"
className="form-control"
id="state"
name="state"
value={formData.state}
onChange={handleChange}
placeholder="CA"
disabled={!editing}
/>
</div>
<div className="col-md-3">
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder="12345"
disabled={!editing}
/>
</div>
</div>
<div className="mb-4">
<label htmlFor="country" className="form-label">Country</label>
<input
type="text"
className="form-control"
id="country"
name="country"
value={formData.country}
onChange={handleChange}
placeholder="United States"
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 Profile
</button>
)}
</form>
</div>
</div>
<div className="card mt-4">
<div className="card-body">
<h5 className="card-title">Account Statistics</h5>
<div className="row text-center">
<div className="col-md-4">
<h3 className="text-primary">{stats.itemsListed}</h3>
<p className="text-muted">Items Listed</p>
</div>
<div className="col-md-4">
<h3 className="text-success">{stats.acceptedRentals}</h3>
<p className="text-muted">Accepted Rentals</p>
</div>
<div className="col-md-4">
<h3 className="text-info">{stats.totalRentals}</h3>
<p className="text-muted">Total Rentals</p>
</div>
</div>
</div>
</div>
<div className="card mt-4">
<div className="card-body">
<h5 className="card-title">Account Settings</h5>
<div className="list-group list-group-flush">
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<i className="bi bi-bell me-2"></i>
Notification Settings
</div>
<i className="bi bi-chevron-right"></i>
</a>
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<i className="bi bi-shield-lock me-2"></i>
Privacy & Security
</div>
<i className="bi bi-chevron-right"></i>
</a>
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<i className="bi bi-credit-card me-2"></i>
Payment Methods
</div>
<i className="bi bi-chevron-right"></i>
</a>
<button
onClick={logout}
className="list-group-item list-group-item-action d-flex justify-content-between align-items-center text-danger border-0 w-100 text-start"
>
<div>
<i className="bi bi-box-arrow-right me-2"></i>
Log Out
</div>
<i className="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Profile;