'use client' import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react'; import { IconShieldAdd } from '@/app/ui/icons/icons'; import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload'; import { useTranslations } from 'next-intl'; import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker'; interface SelectedFile { file: File; preview: string; name: string; size: string; dimensions?: { width: number; height: number; }; } interface FileUploadInitResponse { upload_id: string, file_name: string, total_chunks: number, chunk_size: number, status: string } interface UploadSectionFile { fileType: string allowedExtensions: string[] maxFileSize: number } export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [error, setError] = useState(null); const [uploadId, setUploadId] = useState(null); const [isFileUploaded, setIsFileUploaded] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const t = useTranslations('Global'); let SUPPORTED_FORMATS: string[] = []; switch (fileType) { case 'image': SUPPORTED_FORMATS = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml', 'image/tiff', 'image/x-icon' ]; break; case 'video': SUPPORTED_FORMATS = [ 'video/mp4', 'video/mpeg', 'video/ogg', 'video/webm', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', 'video/x-flv', 'video/3gpp', 'video/3gpp2' ]; break; case 'audio': SUPPORTED_FORMATS = [ 'audio/mpeg', 'audio/wav', 'audio/wave', 'audio/x-wav', 'audio/x-pn-wav', 'audio/ogg', 'audio/webm', 'audio/aac', 'audio/mp4', 'audio/x-m4a', 'audio/flac', 'audio/x-flac', 'audio/opus' ]; break; default: break; } const acceptString = useMemo(() => { if (!allowedExtensions || !Array.isArray(allowedExtensions)) { return ''; } return allowedExtensions.map(e => `.${e}`).join(', '); }, [allowedExtensions]); const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => { if (!SUPPORTED_FORMATS.includes(file.type as string)) { const extension = file.name.split('.').pop()?.toLowerCase(); if (!extension || !allowedExtensions.includes(extension as string)) { return { isValid: false, errorMessage: t('unsupported-file-format') }; } } const MAX_SIZE = maxFileSize; if (file.size > MAX_SIZE) { return { isValid: false, errorMessage: t('the-file-is-too-large') }; } return { isValid: true }; }; const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => { return new Promise((resolve, reject) => { const img = new Image(); const objectUrl = URL.createObjectURL(file); img.onload = () => { const dimensions = { width: img.width, height: img.height }; URL.revokeObjectURL(objectUrl); resolve(dimensions); }; img.onerror = () => { URL.revokeObjectURL(objectUrl); reject(new Error(t('error-uploading-file'))); }; img.src = objectUrl; }); }; const handleFileSelect = useCallback(async (file: File | null): Promise => { if (!file) { setError(null); return; } setUploadProgress(0); setError(null); const validation = validateFile(file); if (!validation.isValid) { setError(validation.errorMessage || t('unknown-validation-error')); return; } try { let dimensions = undefined; if (fileType === "image") { dimensions = await getImageDimensions(file) as { width: number, height: number }; if (dimensions.width < 150 || dimensions.height < 150 || dimensions.width > 10000 || dimensions.height > 10000) { setError(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 150x150 - 10000x10000 ${t('pixels')}.`); return; } } const objectUrl = URL.createObjectURL(file); setSelectedFile({ file, preview: objectUrl, name: file.name, size: `${(file.size / 1024 / 1024).toFixed(2)} MB`, ...(dimensions && { dimensions }) }); console.log('Файл обработан:', { file: file, name: file.name, type: file.type, size: file.size, ...(dimensions && { dimensions }) }); } catch (err) { setError(t('error-processing-file')); } }, []); const handleFileInputChange = (event: ChangeEvent): void => { const file = event.target.files?.[0] || null; handleFileSelect(file); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleButtonClick = (): void => { if (fileInputRef.current) { fileInputRef.current.click(); } }; const handleDragOver = (event: DragEvent): void => { event.preventDefault(); setIsDragging(true); }; const handleDragLeave = (): void => { setIsDragging(false); }; const handleDrop = (event: DragEvent): void => { event.preventDefault(); setIsDragging(false); const file = event.dataTransfer.files[0]; handleFileSelect(file); }; const handleClearFile = async (): Promise => { if (selectedFile) { URL.revokeObjectURL(selectedFile.preview); } if (uploadId) { await cancelUpload(uploadId); } setIsFileUploaded(false); setSelectedFile(null); setError(null); setUploadProgress(0); }; const handlerFileUpload = useCallback(async (file: SelectedFile): Promise => { // Защита от повторного вызова if (isUploading) return; // Сбрасываем состояния setError(null); setIsUploading(true); try { const extension = file.name.split('.').pop(); if (!extension) { setError(t('file-has-no-extension')); return; } const initMessageBody = { file_name: file.name, file_type: fileType, extension: extension, file_size: file.file.size }; const response = await fileUpload(initMessageBody) as FileUploadInitResponse; if (response?.upload_id) { setUploadId(response.upload_id); const CHUNK_SIZE = response.chunk_size; const totalChunks = response.total_chunks; const fileSize = file.file.size; let start = 0; let chunkIndex = 0; console.log('start upload'); console.log(`total chanks - ${totalChunks}`); while (start < fileSize) { console.log(`chank-${chunkIndex}`); const end = Math.min(start + CHUNK_SIZE, fileSize); const chunk = file.file.slice(start, end); const formData = new FormData(); formData.append('upload_id', response.upload_id); formData.append('chunk_number', chunkIndex.toString()); formData.append('chunk', chunk); console.log(`init chunkUpload`); const chunkResponse = await chunkUpload(formData); console.log(`chunkResponse - ${chunkResponse}`); if (chunkResponse.message_desc !== 'Chunk uploaded successfully') { throw new Error('Er1: response error'); } if (chunkIndex === totalChunks - 1) { const chunkStatus = await checkChunkStatus(response.upload_id); if (chunkStatus.message_body.missing_chunks === 0) { setUploadId(null); setIsFileUploaded(true); } else { throw new Error('Er2: not all chunks was uploaded'); } } start = end; chunkIndex++; setUploadProgress(Math.floor(chunkIndex / totalChunks * 100)); } } else { console.error(response); throw new Error('Failed to get upload_id'); } } catch (error) { setError(t('error-uploading-file')); console.error('Chunk upload error:', error); // Для дебага в production if (error instanceof Error) { console.error('Error name:', error.name); console.error('Error message:', error.message); if (error.cause) console.error('Error cause:', error.cause); } } finally { setIsUploading(false); setUploadId(null); } }, [isUploading, fileType]); // Добавьте зависимости useEffect(() => { // Обработка закрытия вкладки // Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob) // он нужен для того что бы послать запрос на бек при закрытии вкладки, // но у него есть свои особенности и стандартные запросы не работают. const handleUnload = () => { if (uploadId) { //const data = JSON.stringify({ uploadId, reason: 'tab_closed' }); //const blob = new Blob([data], { type: 'application/json' }); //navigator.sendBeacon('/api/v1/data/cancel', blob); console.log('sendBeacon'); } }; if (uploadId) { window.addEventListener('unload', handleUnload); } return () => { window.removeEventListener('unload', handleUnload); }; }, [uploadId]); useNavigationBlocker({ shouldBlock: !!uploadId, message: t('have-unsaved-changes'), onConfirm: async () => { console.log('User confirmed navigation'); if (uploadId) { await cancelUpload(uploadId); } }, onCancel: () => { console.log('User cancelled navigation'); } }); return (

{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}

{t('drag-the-file-here')}

{t('or')}

{fileType === "image" && (

{t('image-resolution')}: 100x100 - 10000x10000

)}

{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}

{t('file-format')}: {acceptString}

{error && (

{error}

)} {selectedFile && (

{t('file')}: {selectedFile.name}

{t('size')}: {selectedFile.size}

{t('cost-of-protection')}: 0

{t('current-balance')}: 0

{isFileUploaded && (
{t('file-successfully-uploaded')}
)}
{fileType === 'image' && ( Предпросмотр загруженного изображения setError('Не удалось загрузить превью изображения')} /> )}
{uploadProgress !== 0 && (
{uploadProgress}%
)}
)}
); }