Initial commit - Rentall App
- Full-stack rental marketplace application - React frontend with TypeScript - Node.js/Express backend with JWT authentication - Features: item listings, rental requests, calendar availability, user profiles
This commit is contained in:
381
frontend/src/pages/Profile.tsx
Normal file
381
frontend/src/pages/Profile.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
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 AddressAutocomplete from '../components/AddressAutocomplete';
|
||||
|
||||
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: '',
|
||||
address: '',
|
||||
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 || '',
|
||||
address: response.data.address || '',
|
||||
profileImage: response.data.profileImage || ''
|
||||
});
|
||||
if (response.data.profileImage) {
|
||||
setImagePreview(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 = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
...formData,
|
||||
profileImage: imagePreview || formData.profileImage
|
||||
};
|
||||
|
||||
const response = await userAPI.updateProfile(updateData);
|
||||
setProfileData(response.data);
|
||||
updateUser(response.data); // Update the auth context
|
||||
setSuccess('Profile updated successfully!');
|
||||
setEditing(false);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to update profile');
|
||||
}
|
||||
};
|
||||
|
||||
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 || '',
|
||||
address: profileData.address || '',
|
||||
profileImage: profileData.profileImage || ''
|
||||
});
|
||||
setImagePreview(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}
|
||||
required
|
||||
/>
|
||||
</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="mb-4">
|
||||
<label htmlFor="address" className="form-label">Address</label>
|
||||
{editing ? (
|
||||
<AddressAutocomplete
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={(value) => {
|
||||
setFormData(prev => ({ ...prev, address: value }));
|
||||
}}
|
||||
placeholder="Enter your address"
|
||||
required={false}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</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;
|
||||
Reference in New Issue
Block a user