started dockerfiles and itemrequest

This commit is contained in:
jackiettran
2025-08-18 12:24:46 -04:00
parent 209d8f5fbf
commit 99eae4774e
18 changed files with 2080 additions and 1 deletions

View 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;

View File

@@ -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

View 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;