2025-12-23 18:56:48 +07:00
|
|
|
'use client'
|
|
|
|
|
|
2025-12-25 13:19:32 +07:00
|
|
|
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect } from 'react';
|
2025-12-23 18:56:48 +07:00
|
|
|
import { IconShieldAdd } from '@/app/ui/icons/icons';
|
2025-12-24 15:41:06 +07:00
|
|
|
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/file-upload';
|
|
|
|
|
import { useTranslations } from 'next-intl';
|
2025-12-25 13:19:32 +07:00
|
|
|
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
2025-12-23 18:56:48 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 15:41:06 +07:00
|
|
|
interface UploadSectionFile {
|
|
|
|
|
fileType: string
|
2025-12-24 21:36:59 +07:00
|
|
|
allowedExtensions: string[]
|
|
|
|
|
maxFileSize: number
|
2025-12-24 15:41:06 +07:00
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) {
|
2025-12-23 18:56:48 +07:00
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2025-12-24 15:41:06 +07:00
|
|
|
const [uploadId, setUploadId] = useState<string | null>(null);
|
|
|
|
|
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
2025-12-24 21:36:59 +07:00
|
|
|
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 15:41:06 +07:00
|
|
|
const t = useTranslations('Global');
|
2025-12-24 21:36:59 +07:00
|
|
|
let SUPPORTED_FORMATS: string[] = [];
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
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;
|
2025-12-24 15:41:06 +07:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
const ALLOWED_EXTENSIONS: string[] = allowedExtensions;
|
2025-12-23 18:56:48 +07:00
|
|
|
|
|
|
|
|
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
2025-12-24 15:41:06 +07:00
|
|
|
if (!SUPPORTED_FORMATS.includes(file.type as string)) {
|
2025-12-23 18:56:48 +07:00
|
|
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
2025-12-24 21:36:59 +07:00
|
|
|
if (!extension || !ALLOWED_EXTENSIONS.includes(extension as string)) {
|
2025-12-23 18:56:48 +07:00
|
|
|
return {
|
|
|
|
|
isValid: false,
|
|
|
|
|
errorMessage: 'Неподдерживаемый формат файла.'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
const MAX_SIZE = maxFileSize;
|
|
|
|
|
|
2025-12-23 18:56:48 +07:00
|
|
|
if (file.size > MAX_SIZE) {
|
|
|
|
|
return {
|
|
|
|
|
isValid: false,
|
2025-12-24 21:36:59 +07:00
|
|
|
errorMessage: 'Файл слишком большой. Максимальный размер: 1000MB'
|
2025-12-23 18:56:48 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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('Не удалось загрузить фаил'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.src = objectUrl;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
|
|
|
|
if (!file) {
|
|
|
|
|
setError(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-24 21:36:59 +07:00
|
|
|
setUploadProgress(0);
|
2025-12-23 18:56:48 +07:00
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
const validation = validateFile(file);
|
|
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
setError(validation.errorMessage || 'Неизвестная ошибка валидации');
|
2025-12-24 21:36:59 +07:00
|
|
|
|
2025-12-23 18:56:48 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-24 21:36:59 +07:00
|
|
|
let dimensions = undefined;
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
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(`Размер изображения: ${dimensions.width}x${dimensions.height}. Допустимый диапазон: 150x150 - 10000x10000 пикселей.`);
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const objectUrl = URL.createObjectURL(file);
|
|
|
|
|
|
|
|
|
|
setSelectedFile({
|
|
|
|
|
file,
|
|
|
|
|
preview: objectUrl,
|
|
|
|
|
name: file.name,
|
|
|
|
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
2025-12-24 21:36:59 +07:00
|
|
|
...(dimensions && { dimensions })
|
2025-12-23 18:56:48 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('Файл обработан:', {
|
2025-12-24 21:36:59 +07:00
|
|
|
file: file,
|
2025-12-23 18:56:48 +07:00
|
|
|
name: file.name,
|
|
|
|
|
type: file.type,
|
|
|
|
|
size: file.size,
|
2025-12-24 21:36:59 +07:00
|
|
|
...(dimensions && { dimensions })
|
2025-12-23 18:56:48 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Ошибка при обработке файла');
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): 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<HTMLDivElement>): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setIsDragging(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragLeave = (): void => {
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
|
|
|
|
|
const file = event.dataTransfer.files[0];
|
|
|
|
|
handleFileSelect(file);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClearFile = async (): Promise<void> => {
|
|
|
|
|
if (selectedFile) {
|
|
|
|
|
URL.revokeObjectURL(selectedFile.preview);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 15:41:06 +07:00
|
|
|
if (uploadId) {
|
|
|
|
|
await cancelUpload(uploadId);
|
|
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 15:41:06 +07:00
|
|
|
setIsFileUploaded(false);
|
2025-12-23 18:56:48 +07:00
|
|
|
setSelectedFile(null);
|
|
|
|
|
setError(null);
|
2025-12-24 21:36:59 +07:00
|
|
|
setUploadProgress(0);
|
2025-12-23 18:56:48 +07:00
|
|
|
};
|
|
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
const handlerFileUpload = async (file: SelectedFile): Promise<void> => {
|
|
|
|
|
const extension = file.name.split('.').pop();
|
|
|
|
|
|
|
|
|
|
if (!extension) {
|
|
|
|
|
setError(t('file-has-no-extension'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 18:56:48 +07:00
|
|
|
const initMessageBody = {
|
|
|
|
|
file_name: file.name,
|
|
|
|
|
file_type: fileType,
|
2025-12-24 21:36:59 +07:00
|
|
|
extension: extension,
|
2025-12-23 18:56:48 +07:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
while (start < fileSize) {
|
|
|
|
|
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);
|
|
|
|
|
const chunkResponse = await chunkUpload(formData);
|
|
|
|
|
|
2025-12-24 15:41:06 +07:00
|
|
|
if (chunkResponse.message_desc !== 'Chunk uploaded successfully') {
|
|
|
|
|
throw 'Er1'
|
|
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
|
|
|
|
|
if (chunkIndex === totalChunks - 1) {
|
2025-12-24 15:41:06 +07:00
|
|
|
const chunkStatus = await checkChunkStatus(response.upload_id);
|
|
|
|
|
if (chunkStatus.message_body.missing_chunks === 0) {
|
|
|
|
|
setUploadId(null);
|
|
|
|
|
setIsFileUploaded(true);
|
|
|
|
|
} else {
|
|
|
|
|
throw 'Er2'
|
|
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start = end;
|
|
|
|
|
chunkIndex++;
|
2025-12-24 21:36:59 +07:00
|
|
|
setUploadProgress(
|
|
|
|
|
() => {
|
|
|
|
|
return Math.floor(chunkIndex / totalChunks * 100);
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-12-23 18:56:48 +07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-12-24 15:41:06 +07:00
|
|
|
setError('Chunk uploaded error ' + error);
|
|
|
|
|
console.error('Chunk upload error:', error);
|
2025-12-23 18:56:48 +07:00
|
|
|
}
|
2025-12-24 21:36:59 +07:00
|
|
|
} else {
|
|
|
|
|
console.log(response);
|
|
|
|
|
setError('file uploaded error');
|
2025-12-23 18:56:48 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 13:19:32 +07:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-23 18:56:48 +07:00
|
|
|
return (
|
|
|
|
|
<div className="upload-section">
|
2025-12-24 15:41:06 +07:00
|
|
|
<h3>
|
2025-12-24 21:36:59 +07:00
|
|
|
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
|
2025-12-24 15:41:06 +07:00
|
|
|
</h3>
|
2025-12-23 18:56:48 +07:00
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDragLeave={handleDragLeave}
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
style={{
|
|
|
|
|
border: isDragging ? '2px dashed #3b82f6' : '2px dashed #d1d5db',
|
|
|
|
|
backgroundColor: isDragging ? '#eff6ff' : '#f9fafb',
|
|
|
|
|
padding: '2rem',
|
|
|
|
|
borderRadius: '.75rem',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
transition: 'all .2s ease-in-out'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<IconShieldAdd />
|
2025-12-24 15:41:06 +07:00
|
|
|
<h4>
|
|
|
|
|
{t('drag-the-file-here')}
|
|
|
|
|
</h4>
|
|
|
|
|
<p className="mb-2.5 text-[#6b7280]">
|
|
|
|
|
{t('or')}
|
|
|
|
|
</p>
|
2025-12-23 18:56:48 +07:00
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
2025-12-24 15:41:06 +07:00
|
|
|
accept={
|
|
|
|
|
ALLOWED_EXTENSIONS.map(e => `.${e}`).join(',')
|
|
|
|
|
}
|
2025-12-23 18:56:48 +07:00
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
onChange={handleFileInputChange}
|
|
|
|
|
aria-label="Выбор файла для защиты"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={handleButtonClick}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
2025-12-24 15:41:06 +07:00
|
|
|
{t('select-files-to-protect')}
|
2025-12-23 18:56:48 +07:00
|
|
|
</button>
|
|
|
|
|
|
2025-12-25 13:19:32 +07:00
|
|
|
{fileType === "IMAGE" && (
|
|
|
|
|
<p className="description">
|
|
|
|
|
Разрешение изображение: 100x100 - 10000x10000
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-12-23 18:56:48 +07:00
|
|
|
<p className="description">
|
2025-12-25 13:19:32 +07:00
|
|
|
Размер файла: до {(maxFileSize / 1024 / 1024)} МБ
|
2025-12-23 18:56:48 +07:00
|
|
|
</p>
|
|
|
|
|
<p className="description">
|
2025-12-25 13:19:32 +07:00
|
|
|
Формат файла: {ALLOWED_EXTENSIONS.join(', ')}
|
2025-12-23 18:56:48 +07:00
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
2025-12-24 15:41:06 +07:00
|
|
|
<p className="text-red-600 font-medium">{error}</p>
|
2025-12-23 18:56:48 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{selectedFile && (
|
|
|
|
|
<div
|
|
|
|
|
className="selected-file"
|
|
|
|
|
>
|
|
|
|
|
<div className="selected-file-file-info">
|
2025-12-24 15:41:06 +07:00
|
|
|
<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">Стоимость защиты:</span> 0
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-gray-700">
|
|
|
|
|
<span className="font-medium">Текущий баланс:</span> 0
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
{isFileUploaded && (
|
|
|
|
|
<div className="font-medium text-green-600 text-2xl">
|
|
|
|
|
{t('file-successfully-uploaded')}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-23 18:56:48 +07:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
{fileType === 'IMAGE' && (
|
|
|
|
|
<img
|
|
|
|
|
src={selectedFile.preview}
|
|
|
|
|
alt="Предпросмотр загруженного изображения"
|
|
|
|
|
className="uploaded-image"
|
|
|
|
|
onError={() => setError('Не удалось загрузить превью изображения')}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-23 18:56:48 +07:00
|
|
|
|
2025-12-24 21:36:59 +07:00
|
|
|
<div className="mt-4 flex gap-3 justify-center relative">
|
2025-12-23 18:56:48 +07:00
|
|
|
<button
|
|
|
|
|
className="btn btn-cancel"
|
|
|
|
|
onClick={handleClearFile}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
2025-12-24 15:41:06 +07:00
|
|
|
{isFileUploaded ? t('close') : t('cancel')}
|
2025-12-23 18:56:48 +07:00
|
|
|
</button>
|
|
|
|
|
<button
|
2025-12-24 21:36:59 +07:00
|
|
|
className={`btn btn-confirm ${error ? 'disabled' : ''}`}
|
2025-12-23 18:56:48 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
handlerFileUpload(selectedFile);
|
|
|
|
|
}}
|
|
|
|
|
type="button"
|
2025-12-24 21:36:59 +07:00
|
|
|
disabled={error ? true : false}
|
2025-12-23 18:56:48 +07:00
|
|
|
>
|
2025-12-24 15:41:06 +07:00
|
|
|
{t('upload-file')}
|
|
|
|
|
</button>
|
2025-12-24 21:36:59 +07:00
|
|
|
{uploadProgress !== 0 && (
|
|
|
|
|
<div
|
|
|
|
|
className={`absolute top-5 right-5 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
{uploadProgress}%
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-23 18:56:48 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|