add upload multiple files
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "no-copy-frontend",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.37.0",
|
"version": "0.38.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 2999",
|
"dev": "next dev -p 2999",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/a
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
interface SelectedFile {
|
interface SelectedFile {
|
||||||
file: File;
|
file: File;
|
||||||
preview: string | undefined;
|
preview: string | undefined;
|
||||||
@@ -15,32 +17,41 @@ interface SelectedFile {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
uploadId?: string;
|
||||||
|
progress: number;
|
||||||
|
status: 'pending' | 'uploading' | 'completed' | 'error' | 'cancelled';
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileUploadInitResponse {
|
interface FileUploadInitResponse {
|
||||||
upload_id: string,
|
upload_id: string;
|
||||||
file_name: string,
|
file_name: string;
|
||||||
total_chunks: number,
|
total_chunks: number;
|
||||||
chunk_size: number,
|
chunk_size: number;
|
||||||
status: string
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadSectionFile {
|
interface UploadSectionFile {
|
||||||
fileType: string
|
fileType: string;
|
||||||
allowedExtensions: string[]
|
allowedExtensions: string[];
|
||||||
maxFileSize: number
|
maxFileSize: number;
|
||||||
|
maxParallelUploads?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) {
|
export default function UploadSectionFile({
|
||||||
|
fileType,
|
||||||
|
allowedExtensions,
|
||||||
|
maxFileSize,
|
||||||
|
maxParallelUploads = 3
|
||||||
|
}: UploadSectionFile) {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [uploadId, setUploadId] = useState<string | null>(null);
|
const [activeUploads, setActiveUploads] = useState<Set<string>>(new Set()); // ID активных загрузок
|
||||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
|
||||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isCancelledRef = useRef(false);
|
const isCancelledRef = useRef<Map<string, boolean>>(new Map()); // Для отслеживания отмен по файлам
|
||||||
|
const selectedFilesRef = useRef<SelectedFile[]>(selectedFiles);
|
||||||
|
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
@@ -74,11 +85,6 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
return { isValid: true };
|
return { isValid: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
function setErrorHandler(message: string | null): void {
|
|
||||||
setError(message);
|
|
||||||
setUploadProgress(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -102,63 +108,61 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
const handleFilesSelect = useCallback(async (files: File[]): Promise<void> => {
|
||||||
if (!file) {
|
|
||||||
setError(null);
|
setError(null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(null);
|
const newFiles: SelectedFile[] = [];
|
||||||
setIsFileUploaded(false);
|
|
||||||
setUploadProgress(0);
|
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
const validation = validateFile(file);
|
const validation = validateFile(file);
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
setError(validation.errorMessage || t('unknown-validation-error'));
|
setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dimensions = undefined;
|
let dimensions: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
} | undefined = undefined;
|
||||||
if (fileType === "image") {
|
if (fileType === "image") {
|
||||||
try {
|
try {
|
||||||
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
||||||
if (dimensions.width < 100 || dimensions.height < 100 ||
|
if (dimensions.width < 100 || dimensions.height < 100 ||
|
||||||
dimensions.width > 10000 || dimensions.height > 10000) {
|
dimensions.width > 10000 || dimensions.height > 10000) {
|
||||||
setErrorHandler(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
|
||||||
return;
|
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorHandler(t('error-reading-image'));
|
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFile({
|
newFiles.push({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||||
dimensions,
|
dimensions,
|
||||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
|
||||||
|
progress: 0,
|
||||||
|
status: 'pending'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Файл выбран:', file.name, file.size);
|
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||||
}, [fileType, t]);
|
}, [fileType, t]);
|
||||||
|
|
||||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
const file = event.target.files?.[0] || null;
|
const files = Array.from(event.target.files || []);
|
||||||
handleFileSelect(file);
|
handleFilesSelect(files);
|
||||||
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleButtonClick = (): void => {
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -169,45 +173,73 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
const file = event.dataTransfer.files[0];
|
const files = Array.from(event.dataTransfer.files);
|
||||||
handleFileSelect(file);
|
handleFilesSelect(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearFile = async (): Promise<void> => {
|
const handleClearFile = async (fileId?: string): Promise<void> => {
|
||||||
if (selectedFile && selectedFile.preview) {
|
if (fileId) {
|
||||||
URL.revokeObjectURL(selectedFile.preview);
|
// Очистка конкретного файла
|
||||||
|
const fileToClear = selectedFiles.find(f => f.uploadId === fileId || f.name === fileId);
|
||||||
|
if (fileToClear?.preview) {
|
||||||
|
URL.revokeObjectURL(fileToClear.preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadId) {
|
if (fileToClear?.uploadId) {
|
||||||
await cancelUpload(uploadId);
|
await cancelUpload(fileToClear.uploadId);
|
||||||
|
isCancelledRef.current.set(fileToClear.uploadId, true);
|
||||||
|
setActiveUploads(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(fileToClear.uploadId!);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isCancelledRef.current = true;
|
setSelectedFiles(prev => prev.filter(f =>
|
||||||
|
f.uploadId !== fileId && f.name !== fileId
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Очистка всех файлов
|
||||||
|
selectedFiles.forEach(file => {
|
||||||
|
if (file.preview) {
|
||||||
|
URL.revokeObjectURL(file.preview);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отмена всех активных загрузок
|
||||||
|
const cancelPromises = Array.from(activeUploads).map(uploadId =>
|
||||||
|
cancelUpload(uploadId)
|
||||||
|
);
|
||||||
|
await Promise.allSettled(cancelPromises);
|
||||||
|
|
||||||
|
isCancelledRef.current.clear();
|
||||||
|
setActiveUploads(new Set());
|
||||||
|
setSelectedFiles([]);
|
||||||
|
}
|
||||||
|
|
||||||
setIsFileUploaded(false);
|
|
||||||
setSelectedFile(null);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setUploadProgress(0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
const uploadSingleFile = async (fileInfo: SelectedFile): Promise<void> => {
|
||||||
if (uploadId) return;
|
const fileId = `${fileInfo.name}-${Date.now()}`;
|
||||||
|
isCancelledRef.current.set(fileId, false);
|
||||||
setError(null);
|
|
||||||
isCancelledRef.current = false;
|
|
||||||
const file = fileInfo.file;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const extension = file.name.split('.').pop() || '';
|
setSelectedFiles(prev => prev.map(f =>
|
||||||
|
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
|
||||||
|
));
|
||||||
|
|
||||||
|
const extension = fileInfo.file.name.split('.').pop() || '';
|
||||||
const initMessageBody = {
|
const initMessageBody = {
|
||||||
file_name: file.name,
|
file_name: fileInfo.file.name,
|
||||||
file_type: fileType,
|
file_type: fileType,
|
||||||
extension: extension,
|
extension: extension,
|
||||||
file_size: file.size
|
file_size: fileInfo.file.size
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
||||||
@@ -216,19 +248,23 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
throw new Error('Failed to get upload_id');
|
throw new Error('Failed to get upload_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedFiles(prev => prev.map(f =>
|
||||||
|
f.name === fileInfo.name ? { ...f, uploadId: response.upload_id } : f
|
||||||
|
));
|
||||||
|
|
||||||
|
setActiveUploads(prev => new Set(prev).add(response.upload_id));
|
||||||
|
|
||||||
const CHUNK_SIZE = response.chunk_size;
|
const CHUNK_SIZE = response.chunk_size;
|
||||||
const totalChunks = response.total_chunks;
|
const totalChunks = response.total_chunks;
|
||||||
|
|
||||||
setUploadId(response.upload_id);
|
|
||||||
|
|
||||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||||
if (isCancelledRef.current) {
|
if (isCancelledRef.current.get(fileId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = chunkIndex * CHUNK_SIZE;
|
const start = chunkIndex * CHUNK_SIZE;
|
||||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
const end = Math.min(start + CHUNK_SIZE, fileInfo.file.size);
|
||||||
const chunk = file.slice(start, end);
|
const chunk = fileInfo.file.slice(start, end);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('upload_id', response.upload_id);
|
formData.append('upload_id', response.upload_id);
|
||||||
@@ -241,12 +277,24 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
throw chunkResponse.errorMesage;
|
throw chunkResponse.errorMesage;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
|
const progress = Math.floor((chunkIndex + 1) / totalChunks * 100);
|
||||||
|
setSelectedFiles(prev => prev.map(f =>
|
||||||
|
f.name === fileInfo.name ? { ...f, progress } : f
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||||
setIsFileUploaded(true);
|
setSelectedFiles(prev => prev.map(f =>
|
||||||
|
f.name === fileInfo.name ? { ...f, status: 'completed', progress: 100 } : f
|
||||||
|
));
|
||||||
|
|
||||||
|
setActiveUploads(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(response.upload_id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||||
@@ -255,53 +303,103 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isCancelledRef.current) {
|
if (!isCancelledRef.current.get(fileId)) {
|
||||||
|
const errorMessage = (() => {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case 'duplicate-file':
|
case 'duplicate-file':
|
||||||
setErrorHandler(t('error-duplicate-file'));
|
return t('error-duplicate-file');
|
||||||
break;
|
|
||||||
case 'user-not-have-tokens-for-protect':
|
case 'user-not-have-tokens-for-protect':
|
||||||
setErrorHandler(t('error-user-not-have-tokens-for-protect'));
|
return t('error-user-not-have-tokens-for-protect');
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
setErrorHandler(t('error-uploading-file'));
|
return t('error-uploading-file');
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
setSelectedFiles(prev => prev.map(f =>
|
||||||
|
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setUploadId(null);
|
/* if (fileInfo.uploadId) {
|
||||||
|
setActiveUploads(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(fileInfo.uploadId!);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
} */
|
||||||
}
|
}
|
||||||
}, [uploadId, fileType, t]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
selectedFilesRef.current = selectedFiles;
|
||||||
|
}, [selectedFiles]);
|
||||||
|
|
||||||
|
const handleStartAllUploads = useCallback(async (): Promise<void> => {
|
||||||
|
const uploadNextBatch = async () => {
|
||||||
|
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
|
||||||
|
const batch = currentPending.slice(0, maxParallelUploads);
|
||||||
|
|
||||||
|
if (batch.length === 0) return;
|
||||||
|
|
||||||
|
console.log('Текущие pending файлы:', currentPending);
|
||||||
|
console.log('Загружаемая партия:', batch);
|
||||||
|
|
||||||
|
await Promise.all(batch.map(file => uploadSingleFile(file)));
|
||||||
|
|
||||||
|
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
|
||||||
|
await uploadNextBatch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await uploadNextBatch();
|
||||||
|
}, [maxParallelUploads]);
|
||||||
|
|
||||||
|
const getTotalProgress = useMemo(() => {
|
||||||
|
if (selectedFiles.length === 0) return 0;
|
||||||
|
const total = selectedFiles.reduce((sum, file) => sum + file.progress, 0);
|
||||||
|
return Math.floor(total / selectedFiles.length);
|
||||||
|
}, [selectedFiles]);
|
||||||
|
|
||||||
|
const completedCount = useMemo(() =>
|
||||||
|
selectedFiles.filter(f => f.status === 'completed').length,
|
||||||
|
[selectedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadingCount = useMemo(() =>
|
||||||
|
selectedFiles.filter(f => f.status === 'uploading').length,
|
||||||
|
[selectedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/* useEffect(() => {
|
||||||
// Обработка закрытия вкладки
|
// Обработка закрытия вкладки
|
||||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||||
// но у него есть свои особенности и стандартные запросы не работают.
|
// но у него есть свои особенности и стандартные запросы не работают.
|
||||||
const handleUnload = () => {
|
const handleUnload = () => {
|
||||||
if (uploadId) {
|
if (activeUploads.size) {
|
||||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||||
//const blob = new Blob([data], { type: 'application/json' });
|
//const blob = new Blob([data], { type: 'application/json' });
|
||||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||||
console.log('sendBeacon');
|
console.log('sendBeacon');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (uploadId) {
|
if (activeUploads.size) {
|
||||||
window.addEventListener('unload', handleUnload);
|
window.addEventListener('unload', handleUnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('unload', handleUnload);
|
window.removeEventListener('unload', handleUnload);
|
||||||
};
|
};
|
||||||
}, [uploadId]);
|
}, [activeUploads.size]); */
|
||||||
|
|
||||||
useNavigationBlocker({
|
useNavigationBlocker({
|
||||||
shouldBlock: !!uploadId,
|
shouldBlock: !!activeUploads.size,
|
||||||
message: t('have-unsaved-changes'),
|
message: t('have-unsaved-changes'),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
console.log('User confirmed navigation');
|
console.log('User confirmed navigation');
|
||||||
if (uploadId) {
|
if (activeUploads.size) {
|
||||||
await cancelUpload(uploadId);
|
await handleClearFile();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
@@ -309,13 +407,16 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="upload-section">
|
<div className="upload-section" >
|
||||||
<h3>
|
<h3>
|
||||||
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
|
{t('upload-for-protection', {
|
||||||
|
fileType: t(fileType.toLocaleLowerCase())
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
< div
|
||||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
@@ -325,113 +426,169 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
<h4>
|
<h4>
|
||||||
{t('drag-the-file-here')}
|
{t('drag-the-file-here')}
|
||||||
</h4>
|
</h4>
|
||||||
{/* <p className="mb-2.5 text-[#6b7280]">
|
|
||||||
{t('or')}
|
|
||||||
</p> */}
|
|
||||||
|
|
||||||
{fileType === "image" && (
|
{
|
||||||
<p className="description">
|
fileType === "image" && (
|
||||||
|
<p className="description" >
|
||||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)
|
||||||
<p className="description">
|
}
|
||||||
|
<p className="description" >
|
||||||
{t('file-format')}: {acceptString}
|
{t('file-format')}: {acceptString}
|
||||||
</p>
|
</p>
|
||||||
<p className="description">
|
< p className="description" >
|
||||||
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
< input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept={acceptString}
|
accept={acceptString}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
aria-label="Выбор файла для защиты"
|
aria-label="Выбор файла для защиты"
|
||||||
|
multiple // Добавляем multiple для выбора нескольких файлов
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleButtonClick}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
disabled={uploadingCount > maxParallelUploads}
|
||||||
>
|
>
|
||||||
{t('select-files-to-protect')}
|
{t('select-files-to-protect')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{error && (
|
{
|
||||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
error && (
|
||||||
<p className="text-red-600 font-medium">{error}</p>
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg" >
|
||||||
|
<p className="text-red-600 font-medium" > {error} </p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{selectedFile && (
|
{
|
||||||
|
selectedFiles.length > 0 && (
|
||||||
|
<div className="mt-6" >
|
||||||
|
<div className="flex justify-between items-center mb-4" >
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-700 font-medium" >
|
||||||
|
{t('total-files')}: {selectedFiles.length} |
|
||||||
|
{t('uploading')}: {uploadingCount} |
|
||||||
|
{t('completed')}: {completedCount}
|
||||||
|
</p>
|
||||||
|
< div className="w-full bg-gray-200 rounded-full h-2.5 mt-2" >
|
||||||
<div
|
<div
|
||||||
className={`selected-file ${isFileUploaded ? 'done' : ''}`}
|
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
||||||
>
|
style={{ width: `${getTotalProgress}%` }
|
||||||
<div className="selected-file-file-info">
|
}
|
||||||
<div>
|
> </div>
|
||||||
<p className="text-gray-700">
|
|
||||||
<span className="font-medium">
|
|
||||||
{t('file')}:
|
|
||||||
</span> {selectedFile.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
<span className="font-medium">
|
|
||||||
{t('size')}:
|
|
||||||
</span> {selectedFile.size}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
<span className="font-medium">{t('cost-of-protection')}:</span> 0
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
<span className="font-medium">{t('current-balance')}:</span> 0
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isFileUploaded && (
|
|
||||||
<div className="font-medium text-green-600 text-2xl">
|
|
||||||
{t('file-successfully-uploaded')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(fileType === 'image' && selectedFile.preview) && (
|
< div className="flex gap-2" >
|
||||||
|
<button
|
||||||
|
className={`btn btn-primary`}
|
||||||
|
onClick={handleStartAllUploads}
|
||||||
|
type="button"
|
||||||
|
disabled={uploadingCount > 0 || selectedFiles.every(f => f.status !== 'pending')}
|
||||||
|
>
|
||||||
|
{t('upload-all')}
|
||||||
|
</button>
|
||||||
|
< button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => handleClearFile()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t('clear-all')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
< div className="space-y-4 max-h-96 overflow-y-auto" >
|
||||||
|
{
|
||||||
|
selectedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${index}`}
|
||||||
|
className={`selected-file ${file.status === 'completed' ? 'done' : ''} ${file.status === 'error' ? 'error' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="selected-file-file-info" >
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-700" >
|
||||||
|
<span className="font-medium" > {t('file')}: </span> {file.name}
|
||||||
|
</p>
|
||||||
|
< p className="text-gray-700" >
|
||||||
|
<span className="font-medium" > {t('size')}: </span> {file.size}
|
||||||
|
</p>
|
||||||
|
< p className="text-gray-700" >
|
||||||
|
<span className="font-medium" > {t('status')}: </span>
|
||||||
|
<span className={`ml-2 ${file.status === 'completed' ? 'text-green-600' :
|
||||||
|
file.status === 'uploading' ? 'text-blue-600' :
|
||||||
|
file.status === 'error' ? 'text-red-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{
|
||||||
|
file.status === 'completed' ? t('completed') :
|
||||||
|
file.status === 'uploading' ? t('uploading') :
|
||||||
|
file.status === 'error' ? t('error') :
|
||||||
|
t('pending')
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
file.error && (
|
||||||
|
<p className="text-red-600 text-sm mt-1" > {file.error} </p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
< div className="flex items-center" >
|
||||||
|
<span className={
|
||||||
|
`font-medium text-lg mr-4 ${file.status === 'completed' ? 'text-green-600' :
|
||||||
|
file.status === 'uploading' ? 'text-blue-600' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`
|
||||||
|
}>
|
||||||
|
{file.progress} %
|
||||||
|
</span>
|
||||||
|
< button
|
||||||
|
className="btn btn-cancel text-sm py-1 px-3"
|
||||||
|
onClick={() => handleClearFile(file.uploadId || file.name)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t('remove')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
(fileType === 'image' && file.preview) && (
|
||||||
<img
|
<img
|
||||||
src={selectedFile.preview}
|
src={file.preview}
|
||||||
alt="Предпросмотр загруженного изображения"
|
alt="Предпросмотр загруженного изображения"
|
||||||
className="uploaded-image"
|
className="uploaded-image"
|
||||||
onError={() => setErrorHandler('Не удалось загрузить превью изображения')}
|
onError={() => setError('Не удалось загрузить превью изображения')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex gap-3 justify-center relative">
|
{
|
||||||
|
file.status === 'pending' && (
|
||||||
|
<div className="mt-3 text-center" >
|
||||||
<button
|
<button
|
||||||
className="btn btn-cancel"
|
className="btn btn-confirm text-sm py-1 px-4"
|
||||||
onClick={handleClearFile}
|
onClick={() => uploadSingleFile(file)
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={uploadingCount >= maxParallelUploads}
|
||||||
>
|
>
|
||||||
{isFileUploaded ? t('close') : t('cancel')}
|
{t('start-upload')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className={`btn btn-confirm ${error || isFileUploaded || uploadId ? 'disabled' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
handlerFileUpload(selectedFile);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
disabled={error || isFileUploaded || uploadId ? true : false}
|
|
||||||
>
|
|
||||||
{t('upload-file')}
|
|
||||||
</button>
|
|
||||||
{uploadProgress !== 0 && (
|
|
||||||
<div
|
|
||||||
className={`absolute top-5 right-5 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
|
||||||
>
|
|
||||||
{uploadProgress}%
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -213,7 +213,14 @@
|
|||||||
"error-reading-image": "Error reading image",
|
"error-reading-image": "Error reading image",
|
||||||
"error-user-not-have-tokens-for-protect": "User not have tokens for protect",
|
"error-user-not-have-tokens-for-protect": "User not have tokens for protect",
|
||||||
"registration-date": "Registration date",
|
"registration-date": "Registration date",
|
||||||
"copied": "Copied"
|
"copied": "Copied",
|
||||||
|
"uploading": "uploading",
|
||||||
|
"clear-all": "clear-all",
|
||||||
|
"upload-all": "upload-all",
|
||||||
|
"remove": "remove",
|
||||||
|
"start-upload": "Start upload",
|
||||||
|
"completed": "completed",
|
||||||
|
"pending": "pending"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
|||||||
@@ -213,7 +213,14 @@
|
|||||||
"error-reading-image": "Ошибка чтения изображения",
|
"error-reading-image": "Ошибка чтения изображения",
|
||||||
"error-user-not-have-tokens-for-protect": "У пользователя нет токенов для защиты.",
|
"error-user-not-have-tokens-for-protect": "У пользователя нет токенов для защиты.",
|
||||||
"registration-date": "Дата регистрации",
|
"registration-date": "Дата регистрации",
|
||||||
"copied": "Скопировано"
|
"copied": "Скопировано",
|
||||||
|
"uploading": "загрузка",
|
||||||
|
"clear-all": "очистить всё",
|
||||||
|
"upload-all": "загрузить всё",
|
||||||
|
"remove": "удалить",
|
||||||
|
"start-upload": "Начать загрузку",
|
||||||
|
"completed": "завершено",
|
||||||
|
"pending": "в ожидании"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
@@ -288,6 +295,6 @@
|
|||||||
"recovery-code": "Код восстановления",
|
"recovery-code": "Код восстановления",
|
||||||
"referal-code": "Реферальный код",
|
"referal-code": "Реферальный код",
|
||||||
"email-invalid": "Неверный адрес электронной почты",
|
"email-invalid": "Неверный адрес электронной почты",
|
||||||
"referral-link-is-not-exist" : "Такой реферальной ссылки не существует."
|
"referral-link-is-not-exist": "Такой реферальной ссылки не существует."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user