Snaplocal / client /src /components /DeliveryPanel.jsx
Kuruva Laxmi
Deployment preparation: Configure Vercel, API URLs, and CORS
f359e2d
Raw
History Blame Contribute Delete
7.1 kB
import { useState, useEffect } from 'react';
import { Upload, Download, Check, AlertCircle, Loader2, X } from 'lucide-react';
import api from '../api/api';
const DeliveryPanel = ({ bookingId, photographerId, role = 'photographer' }) => {
const [delivery, setDelivery] = useState(null);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [showUploadForm, setShowUploadForm] = useState(false);
useEffect(() => {
fetchDelivery();
}, [bookingId]);
const fetchDelivery = async () => {
try {
const res = await api.get(`/delivery/booking/${bookingId}`);
setDelivery(res.data);
} catch (err) {
// Delivery record might not exist yet
console.log('Delivery record not yet created');
} finally {
setLoading(false);
}
};
const handlePhotoUpload = async (e) => {
const files = e.target.files;
if (!files || files.length === 0) return;
setUploading(true);
try {
const photoUrls = Array.from(files).map(file => URL.createObjectURL(file));
const res = await api.post(`/delivery/${delivery._id}/upload`, {
photoUrls,
});
setDelivery(res.data);
alert(`${files.length} photos uploaded!`);
setShowUploadForm(false);
} catch (err) {
alert('Failed to upload photos');
} finally {
setUploading(false);
}
};
const handleCompleteDelivery = async () => {
try {
const res = await api.post(`/delivery/${delivery._id}/complete`, {
downloadLinkExpiryDays: 7,
});
setDelivery(res.data);
alert('Delivery marked as complete!');
} catch (err) {
alert('Failed to complete delivery');
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="animate-spin text-blue-600" size={28} />
</div>
);
}
if (!delivery) {
return (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-amber-700">Delivery not yet created for this booking</p>
</div>
);
}
const statusColor = {
pending: 'gray',
partially_delivered: 'blue',
delivered: 'green',
revision_requested: 'orange',
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold">📦 Photo Delivery</h3>
<span
className={`px-3 py-1 rounded-full text-xs font-bold text-${statusColor[delivery.status]}-700 bg-${statusColor[delivery.status]}-100`}
>
{delivery.status.replace('_', ' ').toUpperCase()}
</span>
</div>
{/* Photos Grid */}
{delivery.photos && delivery.photos.length > 0 && (
<div>
<p className="text-sm font-semibold text-gray-700 mb-3">
{delivery.photos.length} Photos Uploaded
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 max-h-[300px] overflow-y-auto">
{delivery.photos.map((photo, idx) => (
<div key={idx} className="aspect-square bg-gray-200 rounded-lg overflow-hidden relative group">
<img
src={photo.url}
alt={`Photo ${idx + 1}`}
className="w-full h-full object-cover group-hover:opacity-75"
/>
{photo.watermarked && (
<div className="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-2 py-1 rounded">
Watermarked
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Upload Form */}
{role === 'photographer' && delivery.status !== 'delivered' && (
<div>
{!showUploadForm ? (
<button
onClick={() => setShowUploadForm(true)}
className="w-full flex items-center justify-center gap-2 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 font-medium"
>
<Upload size={18} /> Add More Photos
</button>
) : (
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6">
<label className="flex flex-col items-center justify-center cursor-pointer">
<Upload className="text-gray-400 mb-2" size={32} />
<p className="font-semibold text-gray-700">Click to upload photos</p>
<p className="text-xs text-gray-500">or drag and drop</p>
<input
type="file"
onChange={handlePhotoUpload}
disabled={uploading}
multiple
className="hidden"
accept="image/*"
/>
</label>
{uploading && <p className="text-sm text-blue-600 mt-2">Uploading...</p>}
<button
onClick={() => setShowUploadForm(false)}
className="mt-2 text-sm text-gray-600 hover:text-gray-900"
>
Cancel
</button>
</div>
)}
</div>
)}
{/* Action Buttons */}
{role === 'photographer' && delivery.status === 'partially_delivered' && (
<button
onClick={handleCompleteDelivery}
className="w-full flex items-center justify-center gap-2 bg-green-600 text-white py-2 rounded-lg hover:bg-green-700 font-medium"
>
<Check size={18} /> Mark as Delivered
</button>
)}
{/* Customer Download Section */}
{role === 'customer' && delivery.status === 'delivered' && delivery.downloadLink && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-green-700 font-semibold mb-3">
<Check size={20} />
Photos Delivered!
</div>
<p className="text-sm text-green-600 mb-3">
Download link expires on{' '}
{new Date(delivery.linkExpiresAt).toLocaleDateString()}
</p>
<a
href={delivery.downloadLink}
className="inline-flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 font-medium"
>
<Download size={18} /> Download Photos
</a>
</div>
)}
{/* Revision Request */}
{delivery.status === 'revision_requested' && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-amber-700 font-semibold mb-2">
<AlertCircle size={20} />
Revision Requested
</div>
<p className="text-sm text-amber-700 mb-3">{delivery.revisionNotes}</p>
{role === 'photographer' && (
<p className="text-xs text-amber-600">
Please review and upload revised photos
</p>
)}
</div>
)}
</div>
);
};
export default DeliveryPanel;