NeahNew/components/missions/file-upload.tsx
2025-05-24 19:59:08 +02:00

379 lines
12 KiB
TypeScript

"use client";
import React, { useState, useRef, useEffect } from 'react';
import { UploadCloud, X, Check, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useSession } from 'next-auth/react';
import { toast } from '@/components/ui/use-toast';
interface FileUploadProps {
type: 'logo' | 'attachment';
missionId?: string; // Make missionId optional
onUploadComplete?: (data: any) => void;
onFileSelect?: (file: File) => void; // New callback for when file is selected but not uploaded yet
maxSize?: number; // in bytes, default 5MB
acceptedFileTypes?: string;
isNewMission?: boolean; // Flag to indicate if this is a new mission being created
}
export function FileUpload({
type,
missionId,
onUploadComplete,
onFileSelect,
maxSize = 5 * 1024 * 1024, // 5MB
acceptedFileTypes = type === 'logo' ? 'image/*' : '.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png',
isNewMission = false
}: FileUploadProps) {
// Log props on init
console.log('FileUpload component initialized with props:', {
type,
missionId,
hasMissionId: !!missionId,
maxSize,
acceptedFileTypes
});
const { data: session } = useSession();
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const isMounted = useRef(true);
// Cleanup on unmount
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// Log when session changes
useEffect(() => {
console.log('Session state in FileUpload:', {
isSessionLoaded: !!session,
hasUser: !!session?.user,
userId: session?.user?.id
});
}, [session]);
// Log when missionId changes
useEffect(() => {
console.log('MissionId in FileUpload component:', missionId);
}, [missionId]);
// Handle drag events
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const validateFile = (file: File): boolean => {
// Check file size
if (file.size > maxSize) {
setError(`File size exceeds the limit of ${maxSize / (1024 * 1024)}MB`);
return false;
}
// Check file type for logo
if (type === 'logo' && !file.type.startsWith('image/')) {
setError('Only image files are allowed for logo');
return false;
}
// For attachments, check file extension
if (type === 'attachment') {
const ext = file.name.split('.').pop()?.toLowerCase();
const allowedExt = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'jpeg', 'png'];
if (ext && !allowedExt.includes(ext)) {
setError(`File type .${ext} is not allowed. Allowed types: ${allowedExt.join(', ')}`);
return false;
}
}
setError(null);
return true;
};
const handleFileDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const droppedFile = e.dataTransfer.files[0];
if (validateFile(droppedFile)) {
setFile(droppedFile);
// If this is a new mission, call onFileSelect instead of waiting for upload
if (isNewMission && onFileSelect) {
onFileSelect(droppedFile);
}
}
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const selectedFile = e.target.files[0];
if (validateFile(selectedFile)) {
setFile(selectedFile);
// Immediately upload the file
handleUpload(selectedFile);
}
}
};
const handleUpload = async (uploadFile?: File) => {
const fileToUpload = uploadFile || file;
if (!fileToUpload) {
console.error('Upload failed: No file selected');
toast({
title: 'Upload failed',
description: 'No file selected. Please select a file first.',
variant: 'destructive',
});
return;
}
if (!session?.user?.id) {
console.error('Upload failed: No user session');
toast({
title: 'Authentication required',
description: 'You need to be logged in to upload files. Please log in and try again.',
variant: 'destructive',
});
return;
}
// For new missions, just notify through the callback and don't try to upload
if (isNewMission) {
if (onFileSelect) {
onFileSelect(fileToUpload);
}
toast({
title: 'File selected',
description: 'The file will be uploaded when you save the mission.',
variant: 'default',
});
return;
}
// For existing missions, we need a missionId
if (!missionId) {
console.error('Upload failed: Missing mission ID');
toast({
title: 'Upload failed',
description: 'Missing mission ID. Please refresh the page and try again.',
variant: 'destructive',
});
return;
}
console.log('Starting upload process...', {
fileName: fileToUpload.name,
fileSize: fileToUpload.size,
fileType: fileToUpload.type,
missionId,
userId: session.user.id,
uploadType: type
});
setIsUploading(true);
setProgress(0);
try {
// Create form data
const formData = new FormData();
formData.append('file', fileToUpload);
formData.append('missionId', missionId);
formData.append('type', type);
console.log('FormData prepared, sending to API...');
// Upload the file
const response = await fetch('/api/missions/upload', {
method: 'POST',
body: formData
});
console.log('API response received:', {
status: response.status,
statusText: response.statusText,
ok: response.ok
});
if (!response.ok) {
const errorData = await response.json();
console.error('API returned error:', errorData);
throw new Error(errorData.error || 'Upload failed');
}
const result = await response.json();
console.log('Upload successful, result:', result);
// Only update state if the component is still mounted
if (isMounted.current) {
setProgress(100);
// Reset file after successful upload
setTimeout(() => {
if (isMounted.current) {
setFile(null);
setIsUploading(false);
setProgress(0);
// Call the callback if provided
if (onUploadComplete) {
onUploadComplete(result);
}
}
toast({
title: 'File uploaded successfully',
description: type === 'logo' ? 'Logo has been updated' : `${fileToUpload.name} has been added to attachments`,
variant: 'default',
});
}, 1000);
}
} catch (error) {
console.error('Upload error details:', error);
// Only update state if the component is still mounted
if (isMounted.current) {
setIsUploading(false);
}
toast({
title: 'Upload failed',
description: error instanceof Error ? error.message : 'An error occurred during upload',
variant: 'destructive',
});
}
};
const handleCancel = () => {
setFile(null);
setError(null);
};
return (
<div className="w-full">
{!file ? (
<div
className={`border-2 border-dashed rounded-md p-6 text-center transition-colors ${
isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleFileDrop}
>
<div className="flex flex-col items-center justify-center">
<UploadCloud className="h-10 w-10 text-gray-400 mb-2" />
<p className="text-sm mb-2 font-medium text-gray-700">
{type === 'logo' ? 'Upload logo image' : 'Upload attachment'}
</p>
<p className="text-xs text-gray-500 mb-4">
Drag and drop or click to browse
</p>
<Button
variant="outline"
size="sm"
className="bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
onClick={() => fileInputRef.current?.click()}
>
Browse Files
</Button>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept={acceptedFileTypes}
/>
{error && (
<p className="text-xs text-red-500 mt-2">{error}</p>
)}
</div>
</div>
) : (
<div className="border rounded-md p-4 bg-white">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0 h-10 w-10 bg-gray-100 rounded-md flex items-center justify-center">
{type === 'logo' ? (
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="h-full w-full object-cover rounded-md"
/>
) : (
<div className="text-xs font-bold bg-blue-100 text-blue-600 h-full w-full rounded-md flex items-center justify-center">
{file.name.split('.').pop()?.toUpperCase()}
</div>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">
{file.name}
</p>
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(2)} KB
</p>
</div>
</div>
<div className="flex items-center space-x-2">
{isUploading ? (
<div className="flex items-center">
<Loader2 className="animate-spin h-4 w-4 mr-1 text-blue-500" />
<span className="text-xs text-gray-500">{progress}%</span>
</div>
) : (
<>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
{!isNewMission && (
<Button
variant="default"
size="sm"
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => handleUpload()}
>
<Check className="h-4 w-4 mr-1" />
Upload
</Button>
)}
</>
)}
</div>
</div>
{isUploading && (
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-3">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{ width: `${progress}%` }}
></div>
</div>
)}
</div>
)}
</div>
);
}