started dockerfiles and itemrequest
This commit is contained in:
111
frontend/src/components/ItemRequestCard.tsx
Normal file
111
frontend/src/components/ItemRequestCard.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ItemRequest } from '../types';
|
||||
|
||||
interface ItemRequestCardProps {
|
||||
request: ItemRequest;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
const ItemRequestCard: React.FC<ItemRequestCardProps> = ({ request, showActions = true }) => {
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Flexible';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
const getLocationString = () => {
|
||||
const parts = [];
|
||||
if (request.city) parts.push(request.city);
|
||||
if (request.state) parts.push(request.state);
|
||||
return parts.join(', ') || 'Location not specified';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'open':
|
||||
return 'success';
|
||||
case 'fulfilled':
|
||||
return 'primary';
|
||||
case 'closed':
|
||||
return 'secondary';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card h-100 shadow-sm hover-shadow">
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 className="card-title text-truncate flex-grow-1 me-2">{request.title}</h5>
|
||||
<span className={`badge bg-${getStatusColor(request.status)}`}>
|
||||
{request.status.charAt(0).toUpperCase() + request.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="card-text text-muted small mb-2">
|
||||
{request.description.length > 100
|
||||
? `${request.description.substring(0, 100)}...`
|
||||
: request.description
|
||||
}
|
||||
</p>
|
||||
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-geo-alt me-1"></i>
|
||||
{getLocationString()}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-person me-1"></i>
|
||||
Requested by {request.requester?.firstName || 'Unknown'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{(request.maxPricePerDay || request.maxPricePerHour) && (
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-currency-dollar me-1"></i>
|
||||
Budget:
|
||||
{request.maxPricePerDay && ` $${request.maxPricePerDay}/day`}
|
||||
{request.maxPricePerHour && ` $${request.maxPricePerHour}/hour`}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-calendar me-1"></i>
|
||||
Dates: {formatDate(request.preferredStartDate)} - {formatDate(request.preferredEndDate)}
|
||||
{request.isFlexibleDates && ' (Flexible)'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<small className="text-muted">
|
||||
{request.responseCount || 0} response{request.responseCount !== 1 ? 's' : ''}
|
||||
</small>
|
||||
<small className="text-muted">
|
||||
{new Date(request.createdAt).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{showActions && (
|
||||
<div className="d-grid gap-2 mt-3">
|
||||
<Link
|
||||
to={`/item-requests/${request.id}`}
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
>
|
||||
View Details
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemRequestCard;
|
||||
@@ -88,6 +88,11 @@ const Navbar: React.FC = () => {
|
||||
<i className="bi bi-list-ul me-2"></i>My Listings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/my-requests">
|
||||
<i className="bi bi-clipboard-check me-2"></i>My Requests
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/messages">
|
||||
<i className="bi bi-envelope me-2"></i>Messages
|
||||
|
||||
257
frontend/src/components/RequestResponseModal.tsx
Normal file
257
frontend/src/components/RequestResponseModal.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI, itemAPI } from '../services/api';
|
||||
import { ItemRequest, Item } from '../types';
|
||||
|
||||
interface RequestResponseModalProps {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
request: ItemRequest | null;
|
||||
onResponseSubmitted: () => void;
|
||||
}
|
||||
|
||||
const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
show,
|
||||
onHide,
|
||||
request,
|
||||
onResponseSubmitted
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userItems, setUserItems] = useState<Item[]>([]);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
message: '',
|
||||
offerPricePerHour: '',
|
||||
offerPricePerDay: '',
|
||||
availableStartDate: '',
|
||||
availableEndDate: '',
|
||||
existingItemId: '',
|
||||
contactInfo: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (show && user) {
|
||||
fetchUserItems();
|
||||
resetForm();
|
||||
}
|
||||
}, [show, user]);
|
||||
|
||||
const fetchUserItems = async () => {
|
||||
try {
|
||||
const response = await itemAPI.getItems({ owner: user?.id });
|
||||
setUserItems(response.data.items || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user items:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
message: '',
|
||||
offerPricePerHour: '',
|
||||
offerPricePerDay: '',
|
||||
availableStartDate: '',
|
||||
availableEndDate: '',
|
||||
existingItemId: '',
|
||||
contactInfo: ''
|
||||
});
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!request || !user) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const responseData = {
|
||||
...formData,
|
||||
offerPricePerHour: formData.offerPricePerHour ? parseFloat(formData.offerPricePerHour) : null,
|
||||
offerPricePerDay: formData.offerPricePerDay ? parseFloat(formData.offerPricePerDay) : null,
|
||||
existingItemId: formData.existingItemId || null,
|
||||
availableStartDate: formData.availableStartDate || null,
|
||||
availableEndDate: formData.availableEndDate || null
|
||||
};
|
||||
|
||||
await itemRequestAPI.respondToRequest(request.id, responseData);
|
||||
onResponseSubmitted();
|
||||
onHide();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to submit response');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
return (
|
||||
<div className={`modal fade ${show ? 'show d-block' : ''}`} tabIndex={-1} style={{ backgroundColor: show ? 'rgba(0,0,0,0.5)' : 'transparent' }}>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Respond to Request</h5>
|
||||
<button type="button" className="btn-close" onClick={onHide}></button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="mb-3 p-3 bg-light rounded">
|
||||
<h6>{request.title}</h6>
|
||||
<p className="text-muted small mb-0">{request.description}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="message" className="form-label">Your Message *</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="message"
|
||||
name="message"
|
||||
rows={4}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Explain how you can help, availability, condition of the item, etc."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{userItems.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<label htmlFor="existingItemId" className="form-label">Do you have an existing listing for this item?</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="existingItemId"
|
||||
name="existingItemId"
|
||||
value={formData.existingItemId}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">No existing listing</option>
|
||||
{userItems.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name} - ${item.pricePerDay}/day
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="form-text">
|
||||
If you have an existing listing that matches this request, select it here.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="offerPricePerDay" className="form-label">Your Price per Day</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="offerPricePerDay"
|
||||
name="offerPricePerDay"
|
||||
value={formData.offerPricePerDay}
|
||||
onChange={handleChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="offerPricePerHour" className="form-label">Your Price per Hour</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="offerPricePerHour"
|
||||
name="offerPricePerHour"
|
||||
value={formData.offerPricePerHour}
|
||||
onChange={handleChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="availableStartDate" className="form-label">Available From</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
id="availableStartDate"
|
||||
name="availableStartDate"
|
||||
value={formData.availableStartDate}
|
||||
onChange={handleChange}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="availableEndDate" className="form-label">Available Until</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
id="availableEndDate"
|
||||
name="availableEndDate"
|
||||
value={formData.availableEndDate}
|
||||
onChange={handleChange}
|
||||
min={formData.availableStartDate || new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="contactInfo" className="form-label">Contact Information</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="contactInfo"
|
||||
name="contactInfo"
|
||||
value={formData.contactInfo}
|
||||
onChange={handleChange}
|
||||
placeholder="Phone number, email, or preferred contact method"
|
||||
/>
|
||||
<div className="form-text">
|
||||
How should the requester contact you if they're interested?
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !formData.message.trim()}
|
||||
>
|
||||
{loading ? 'Submitting...' : 'Submit Response'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestResponseModal;
|
||||
Reference in New Issue
Block a user