379 lines
12 KiB
TypeScript
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>
|
|
);
|
|
} |