started dockerfiles and itemrequest
This commit is contained in:
362
frontend/src/pages/ItemRequestDetail.tsx
Normal file
362
frontend/src/pages/ItemRequestDetail.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI } from '../services/api';
|
||||
import { ItemRequest, ItemRequestResponse } from '../types';
|
||||
import RequestResponseModal from '../components/RequestResponseModal';
|
||||
|
||||
const ItemRequestDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [request, setRequest] = useState<ItemRequest | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showResponseModal, setShowResponseModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchRequest();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchRequest = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await itemRequestAPI.getItemRequest(id!);
|
||||
setRequest(response.data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to fetch request details');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResponseSubmitted = () => {
|
||||
fetchRequest();
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'open':
|
||||
return 'success';
|
||||
case 'fulfilled':
|
||||
return 'primary';
|
||||
case 'closed':
|
||||
return 'secondary';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const getResponseStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'warning';
|
||||
case 'accepted':
|
||||
return 'success';
|
||||
case 'declined':
|
||||
return 'danger';
|
||||
case 'expired':
|
||||
return 'secondary';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Not specified';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
const getLocationString = () => {
|
||||
if (!request) return '';
|
||||
const parts = [];
|
||||
if (request.city) parts.push(request.city);
|
||||
if (request.state) parts.push(request.state);
|
||||
return parts.join(', ') || 'Location not specified';
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !request) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error || 'Request not found'}
|
||||
</div>
|
||||
<button className="btn btn-secondary" onClick={() => navigate(-1)}>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isOwner = user?.id === request.requesterId;
|
||||
const canRespond = user && !isOwner && request.status === 'open';
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row">
|
||||
<div className="col-lg-8">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h1 className="card-title">{request.title}</h1>
|
||||
<p className="text-muted mb-2">
|
||||
Requested by {request.requester?.firstName || 'Unknown'} {request.requester?.lastName || ''}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`badge bg-${getStatusColor(request.status)} fs-6`}>
|
||||
{request.status.charAt(0).toUpperCase() + request.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h5>Description</h5>
|
||||
<p className="card-text">{request.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="row mb-4">
|
||||
<div className="col-md-6">
|
||||
<h6><i className="bi bi-geo-alt me-2"></i>Location</h6>
|
||||
<p className="text-muted">{getLocationString()}</p>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<h6><i className="bi bi-calendar me-2"></i>Timeline</h6>
|
||||
<p className="text-muted">
|
||||
{request.isFlexibleDates ? (
|
||||
'Flexible dates'
|
||||
) : (
|
||||
`${formatDate(request.preferredStartDate)} - ${formatDate(request.preferredEndDate)}`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(request.maxPricePerDay || request.maxPricePerHour) && (
|
||||
<div className="mb-4">
|
||||
<h6><i className="bi bi-currency-dollar me-2"></i>Budget</h6>
|
||||
<div className="text-muted">
|
||||
{request.maxPricePerDay && <div>Up to ${request.maxPricePerDay} per day</div>}
|
||||
{request.maxPricePerHour && <div>Up to ${request.maxPricePerHour} per hour</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<small className="text-muted">
|
||||
Created on {new Date(request.createdAt).toLocaleDateString()} •
|
||||
{request.responseCount || 0} response{request.responseCount !== 1 ? 's' : ''}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{canRespond && (
|
||||
<div className="d-grid">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowResponseModal(true)}
|
||||
>
|
||||
<i className="bi bi-reply me-2"></i>
|
||||
Respond to Request
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<div className="d-flex gap-2">
|
||||
<Link to={`/my-requests`} className="btn btn-outline-primary">
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
Back to My Requests
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.responses && request.responses.length > 0 && (
|
||||
<div className="card mt-4">
|
||||
<div className="card-body">
|
||||
<h5 className="mb-4">Responses ({request.responses.length})</h5>
|
||||
|
||||
{request.responses.map((response: ItemRequestResponse) => (
|
||||
<div key={response.id} className="border-bottom pb-4 mb-4 last:border-bottom-0 last:pb-0 last:mb-0">
|
||||
<div className="d-flex justify-content-between align-items-start mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="me-3">
|
||||
<div className="bg-light rounded-circle d-flex align-items-center justify-content-center" style={{ width: '40px', height: '40px' }}>
|
||||
<i className="bi bi-person"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{response.responder?.firstName || 'Unknown'} {response.responder?.lastName || ''}</strong>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
{new Date(response.createdAt).toLocaleDateString()} at {new Date(response.createdAt).toLocaleTimeString()}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`badge bg-${getResponseStatusColor(response.status)}`}>
|
||||
{response.status.charAt(0).toUpperCase() + response.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="ms-5">
|
||||
<p className="mb-3">{response.message}</p>
|
||||
|
||||
{(response.offerPricePerDay || response.offerPricePerHour) && (
|
||||
<div className="mb-2">
|
||||
<strong>Offered Price:</strong>
|
||||
<div className="text-muted">
|
||||
{response.offerPricePerDay && <div>${response.offerPricePerDay} per day</div>}
|
||||
{response.offerPricePerHour && <div>${response.offerPricePerHour} per hour</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(response.availableStartDate || response.availableEndDate) && (
|
||||
<div className="mb-2">
|
||||
<strong>Availability:</strong>
|
||||
<div className="text-muted">
|
||||
{formatDate(response.availableStartDate)} - {formatDate(response.availableEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.contactInfo && (
|
||||
<div className="mb-2">
|
||||
<strong>Contact:</strong>
|
||||
<span className="text-muted ms-2">{response.contactInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.existingItem && (
|
||||
<div className="mb-2">
|
||||
<strong>Related Item:</strong>
|
||||
<Link to={`/items/${response.existingItem.id}`} className="ms-2 text-decoration-none">
|
||||
{response.existingItem.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-lg-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h6>Request Summary</h6>
|
||||
|
||||
<div className="mb-3">
|
||||
<strong>Status:</strong>
|
||||
<span className={`badge bg-${getStatusColor(request.status)} ms-2`}>
|
||||
{request.status.charAt(0).toUpperCase() + request.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<strong>Requested by:</strong>
|
||||
<div className="mt-1">
|
||||
<Link to={`/users/${request.requester?.id}`} className="text-decoration-none">
|
||||
{request.requester?.firstName || 'Unknown'} {request.requester?.lastName || ''}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(request.maxPricePerDay || request.maxPricePerHour) && (
|
||||
<div className="mb-3">
|
||||
<strong>Budget Range:</strong>
|
||||
<div className="text-muted mt-1">
|
||||
{request.maxPricePerDay && <div>≤ ${request.maxPricePerDay}/day</div>}
|
||||
{request.maxPricePerHour && <div>≤ ${request.maxPricePerHour}/hour</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<strong>Timeline:</strong>
|
||||
<div className="text-muted mt-1">
|
||||
{request.isFlexibleDates ? (
|
||||
'Flexible dates'
|
||||
) : (
|
||||
<div>
|
||||
<div>From: {formatDate(request.preferredStartDate)}</div>
|
||||
<div>To: {formatDate(request.preferredEndDate)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<strong>Location:</strong>
|
||||
<div className="text-muted mt-1">{getLocationString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<strong>Responses:</strong>
|
||||
<div className="text-muted mt-1">{request.responseCount || 0} received</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="d-grid gap-2">
|
||||
{canRespond ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => setShowResponseModal(true)}
|
||||
>
|
||||
<i className="bi bi-reply me-2"></i>
|
||||
Respond to Request
|
||||
</button>
|
||||
) : user && !isOwner ? (
|
||||
<div className="text-muted text-center">
|
||||
<small>This request is {request.status}</small>
|
||||
</div>
|
||||
) : !user ? (
|
||||
<div className="text-center">
|
||||
<Link to="/login" className="btn btn-outline-primary">
|
||||
Log in to Respond
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RequestResponseModal
|
||||
show={showResponseModal}
|
||||
onHide={() => setShowResponseModal(false)}
|
||||
request={request}
|
||||
onResponseSubmitted={handleResponseSubmitted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemRequestDetail;
|
||||
Reference in New Issue
Block a user