NeahNew/components/missions/attachments-list.tsx
2025-05-06 21:05:35 +02:00

293 lines
9.2 KiB
TypeScript

"use client";
import React, { useState, useEffect } from 'react';
import {
FileText,
Image,
File,
Trash2,
Download,
Loader2,
FileSpreadsheet,
FileArchive,
FileVideo,
FileAudio,
FilePlus
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useSession } from 'next-auth/react';
import { toast } from '@/components/ui/use-toast';
import { FileUpload } from './file-upload';
interface Attachment {
id: string;
filename: string;
filePath: string;
fileType: string;
fileSize: number;
createdAt: string;
}
interface AttachmentsListProps {
missionId: string;
initialAttachments?: Attachment[];
allowUpload?: boolean;
allowDelete?: boolean;
onAttachmentAdded?: (attachment: Attachment) => void;
onAttachmentDeleted?: (attachmentId: string) => void;
}
export function AttachmentsList({
missionId,
initialAttachments = [],
allowUpload = true,
allowDelete = true,
onAttachmentAdded,
onAttachmentDeleted
}: AttachmentsListProps) {
const { data: session } = useSession();
const [attachments, setAttachments] = useState<Attachment[]>(initialAttachments);
const [isLoading, setIsLoading] = useState(false);
const [deleteAttachment, setDeleteAttachment] = useState<Attachment | null>(null);
const [showUpload, setShowUpload] = useState(false);
// Fetch attachments for the mission if not provided initially
useEffect(() => {
if (initialAttachments.length === 0) {
fetchAttachments();
}
}, [missionId]);
const fetchAttachments = async () => {
if (!missionId || !session?.user?.id) return;
setIsLoading(true);
try {
const response = await fetch(`/api/missions/${missionId}/attachments`);
if (!response.ok) {
throw new Error('Failed to fetch attachments');
}
const data = await response.json();
setAttachments(data);
} catch (error) {
console.error('Error fetching attachments:', error);
toast({
title: 'Error',
description: 'Failed to load attachments',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const handleAttachmentUploaded = (data: any) => {
if (data?.attachment) {
setAttachments(prev => [...prev, data.attachment]);
setShowUpload(false);
if (onAttachmentAdded) {
onAttachmentAdded(data.attachment);
}
}
};
const handleAttachmentDelete = async () => {
if (!deleteAttachment) return;
try {
const response = await fetch(`/api/missions/${missionId}/attachments/${deleteAttachment.id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete attachment');
}
setAttachments(prev => prev.filter(a => a.id !== deleteAttachment.id));
if (onAttachmentDeleted) {
onAttachmentDeleted(deleteAttachment.id);
}
toast({
title: 'Success',
description: 'Attachment deleted successfully',
variant: 'default',
});
} catch (error) {
console.error('Error deleting attachment:', error);
toast({
title: 'Error',
description: 'Failed to delete attachment',
variant: 'destructive',
});
} finally {
setDeleteAttachment(null);
}
};
const getFileIcon = (fileType: string) => {
if (fileType.startsWith('image/')) {
return <Image className="h-5 w-5 text-blue-500" />;
} else if (fileType.includes('pdf')) {
return <FileText className="h-5 w-5 text-red-500" />;
} else if (fileType.includes('spreadsheet') || fileType.includes('excel') || fileType.includes('csv')) {
return <FileSpreadsheet className="h-5 w-5 text-green-500" />;
} else if (fileType.includes('zip') || fileType.includes('compressed')) {
return <FileArchive className="h-5 w-5 text-purple-500" />;
} else if (fileType.includes('video')) {
return <FileVideo className="h-5 w-5 text-pink-500" />;
} else if (fileType.includes('audio')) {
return <FileAudio className="h-5 w-5 text-orange-500" />;
} else {
return <File className="h-5 w-5 text-gray-500" />;
}
};
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
};
return (
<div className="space-y-4">
{(allowUpload && !showUpload) && (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setShowUpload(true)}
className="bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
>
<FilePlus className="h-4 w-4 mr-1" />
Add Attachment
</Button>
</div>
)}
{showUpload && (
<div className="mb-6">
<h4 className="text-sm font-medium mb-2 text-gray-700">Upload New Attachment</h4>
<FileUpload
type="attachment"
missionId={missionId}
onUploadComplete={handleAttachmentUploaded}
isNewMission={false}
/>
<div className="mt-2 flex justify-end">
<Button
variant="ghost"
size="sm"
onClick={() => setShowUpload(false)}
className="text-gray-500"
>
Cancel
</Button>
</div>
</div>
)}
{isLoading ? (
<div className="flex justify-center items-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
</div>
) : attachments.length === 0 ? (
<div className="text-center p-8 bg-gray-50 border border-gray-200 rounded-md">
<File className="h-10 w-10 text-gray-300 mx-auto mb-2" />
<p className="text-sm text-gray-500">No attachments yet</p>
{allowUpload && (
<Button
variant="outline"
size="sm"
onClick={() => setShowUpload(true)}
className="mt-4 bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
>
<FilePlus className="h-4 w-4 mr-1" />
Add your first attachment
</Button>
)}
</div>
) : (
<div className="border rounded-md overflow-hidden">
<ul className="divide-y">
{attachments.map((attachment) => (
<li key={attachment.id} className="flex items-center justify-between p-3 hover:bg-gray-50">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
{getFileIcon(attachment.fileType)}
</div>
<div>
<p className="font-medium text-sm text-gray-900">{attachment.filename}</p>
<p className="text-xs text-gray-500">
{formatFileSize(attachment.fileSize)} {new Date(attachment.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex space-x-1">
<Button
variant="ghost"
size="sm"
asChild
className="text-gray-500 hover:text-gray-700 hover:bg-gray-100"
>
<a
href={`/api/missions/${missionId}/attachments/download/${attachment.id}`}
download={attachment.filename}
>
<Download className="h-4 w-4" />
</a>
</Button>
{allowDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => setDeleteAttachment(attachment)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</li>
))}
</ul>
</div>
)}
<AlertDialog open={!!deleteAttachment} onOpenChange={(open) => !open && setDeleteAttachment(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Attachment</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{deleteAttachment?.filename}"? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleAttachmentDelete} className="bg-red-600 hover:bg-red-700 text-white">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}