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:
295
frontend/src/pages/MyListings.tsx
Normal file
295
frontend/src/pages/MyListings.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import api from '../services/api';
|
||||
import { Item, Rental } from '../types';
|
||||
import { rentalAPI } from '../services/api';
|
||||
|
||||
const MyListings: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [listings, setListings] = useState<Item[]>([]);
|
||||
const [rentals, setRentals] = useState<Rental[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchMyListings();
|
||||
fetchRentalRequests();
|
||||
}, [user]);
|
||||
|
||||
const fetchMyListings = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(''); // Clear any previous errors
|
||||
const response = await api.get('/items');
|
||||
|
||||
// Filter items to only show ones owned by current user
|
||||
const myItems = response.data.items.filter((item: Item) => item.ownerId === user.id);
|
||||
setListings(myItems);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching listings:', err);
|
||||
// Only show error for actual API failures
|
||||
if (err.response && err.response.status >= 500) {
|
||||
setError('Failed to get your listings. Please try again later.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (itemId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this listing?')) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/items/${itemId}`);
|
||||
setListings(listings.filter(item => item.id !== itemId));
|
||||
} catch (err: any) {
|
||||
alert('Failed to delete listing');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAvailability = async (item: Item) => {
|
||||
try {
|
||||
await api.put(`/items/${item.id}`, {
|
||||
...item,
|
||||
availability: !item.availability
|
||||
});
|
||||
setListings(listings.map(i =>
|
||||
i.id === item.id ? { ...i, availability: !i.availability } : i
|
||||
));
|
||||
} catch (err: any) {
|
||||
alert('Failed to update availability');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRentalRequests = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const response = await rentalAPI.getMyListings();
|
||||
setRentals(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching rental requests:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcceptRental = async (rentalId: string) => {
|
||||
try {
|
||||
await rentalAPI.updateRentalStatus(rentalId, 'confirmed');
|
||||
// Refresh the rentals list
|
||||
fetchRentalRequests();
|
||||
} catch (err) {
|
||||
console.error('Failed to accept rental request:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectRental = async (rentalId: string) => {
|
||||
try {
|
||||
await api.put(`/rentals/${rentalId}/status`, {
|
||||
status: 'cancelled',
|
||||
rejectionReason: 'Request declined by owner'
|
||||
});
|
||||
// Refresh the rentals list
|
||||
fetchRentalRequests();
|
||||
} catch (err) {
|
||||
console.error('Failed to reject rental request:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<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="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>My Listings</h1>
|
||||
<Link to="/create-item" className="btn btn-primary">
|
||||
Add New Item
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(() => {
|
||||
const pendingCount = rentals.filter(r => r.status === 'pending').length;
|
||||
|
||||
if (pendingCount > 0) {
|
||||
return (
|
||||
<div className="alert alert-info d-flex align-items-center" role="alert">
|
||||
<i className="bi bi-bell-fill me-2"></i>
|
||||
You have {pendingCount} pending rental request{pendingCount > 1 ? 's' : ''} to review.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{listings.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-muted">You haven't listed any items yet.</p>
|
||||
<Link to="/create-item" className="btn btn-primary mt-3">
|
||||
List Your First Item
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row">
|
||||
{listings.map((item) => (
|
||||
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<Link
|
||||
to={`/items/${item.id}/edit`}
|
||||
className="text-decoration-none"
|
||||
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button') || target.closest('.rental-requests')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="card h-100" style={{ cursor: 'pointer' }}>
|
||||
{item.images && item.images[0] && (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{ height: '200px', objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-dark">
|
||||
{item.name}
|
||||
</h5>
|
||||
<p className="card-text text-truncate text-dark">{item.description}</p>
|
||||
|
||||
<div className="mb-2">
|
||||
<span className={`badge ${item.availability ? 'bg-success' : 'bg-secondary'}`}>
|
||||
{item.availability ? 'Available' : 'Not Available'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
{item.pricePerDay && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerDay}/day
|
||||
</div>
|
||||
)}
|
||||
{item.pricePerHour && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerHour}/hour
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
onClick={() => toggleAvailability(item)}
|
||||
className="btn btn-sm btn-outline-info"
|
||||
>
|
||||
{item.availability ? 'Mark Unavailable' : 'Mark Available'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const pendingRentals = rentals.filter(r =>
|
||||
r.itemId === item.id && r.status === 'pending'
|
||||
);
|
||||
const acceptedRentals = rentals.filter(r =>
|
||||
r.itemId === item.id && ['confirmed', 'active'].includes(r.status)
|
||||
);
|
||||
|
||||
if (pendingRentals.length > 0 || acceptedRentals.length > 0) {
|
||||
return (
|
||||
<div className="mt-3 border-top pt-3 rental-requests">
|
||||
{pendingRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-primary mb-2">
|
||||
<i className="bi bi-bell-fill"></i> Pending Requests ({pendingRentals.length})
|
||||
</h6>
|
||||
{pendingRentals.map(rental => (
|
||||
<div key={rental.id} className="border rounded p-2 mb-2 bg-light">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div className="small">
|
||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong><br/>
|
||||
{new Date(rental.startDate).toLocaleDateString()} - {new Date(rental.endDate).toLocaleDateString()}<br/>
|
||||
<span className="text-muted">${rental.totalAmount}</span>
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
onClick={() => handleAcceptRental(rental.id)}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleRejectRental(rental.id)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{acceptedRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-success mb-2 mt-3">
|
||||
<i className="bi bi-check-circle-fill"></i> Accepted Rentals ({acceptedRentals.length})
|
||||
</h6>
|
||||
{acceptedRentals.map(rental => (
|
||||
<div key={rental.id} className="border rounded p-2 mb-2 bg-light">
|
||||
<div className="small">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong><br/>
|
||||
{new Date(rental.startDate).toLocaleDateString()} - {new Date(rental.endDate).toLocaleDateString()}<br/>
|
||||
<span className="text-muted">${rental.totalAmount}</span>
|
||||
</div>
|
||||
<span className={`badge ${rental.status === 'active' ? 'bg-success' : 'bg-info'}`}>
|
||||
{rental.status === 'active' ? 'Active' : 'Confirmed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyListings;
|
||||
Reference in New Issue
Block a user