add upload multiple files

This commit is contained in:
smanylov
2026-02-11 20:11:39 +07:00
parent 459035529c
commit 930e811232
4 changed files with 386 additions and 215 deletions
+1 -1
View File
@@ -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",
+368 -211
View File
@@ -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);
return;
}
setError(null); setError(null);
setIsFileUploaded(false);
setUploadProgress(0);
const validation = validateFile(file); const newFiles: SelectedFile[] = [];
if (!validation.isValid) { for (const file of files) {
setError(validation.errorMessage || t('unknown-validation-error')); const validation = validateFile(file);
return;
}
let dimensions = undefined; if (!validation.isValid) {
if (fileType === "image") { setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
try { continue;
dimensions = await getImageDimensions(file) as { width: number, height: number };
if (dimensions.width < 100 || dimensions.height < 100 ||
dimensions.width > 10000 || dimensions.height > 10000) {
setErrorHandler(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
return;
}
} catch (err) {
setErrorHandler(t('error-reading-image'));
return;
} }
let dimensions: {
width: number,
height: number,
} | undefined = undefined;
if (fileType === "image") {
try {
dimensions = await getImageDimensions(file) as { width: number, height: number };
if (dimensions.width < 100 || dimensions.height < 100 ||
dimensions.width > 10000 || dimensions.height > 10000) {
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
continue;
}
} catch (err) {
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
continue;
}
}
newFiles.push({
file,
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
dimensions,
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
progress: 0,
status: 'pending'
});
} }
setSelectedFile({ setSelectedFiles(prev => [...prev, ...newFiles]);
file,
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
dimensions,
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
});
console.log('Файл выбран:', file.name, file.size);
}, [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 (fileToClear?.uploadId) {
await cancelUpload(fileToClear.uploadId);
isCancelledRef.current.set(fileToClear.uploadId, true);
setActiveUploads(prev => {
const newSet = new Set(prev);
newSet.delete(fileToClear.uploadId!);
return newSet;
});
}
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([]);
} }
if (uploadId) {
await cancelUpload(uploadId);
}
isCancelledRef.current = true;
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)) {
switch (error) { const errorMessage = (() => {
case 'duplicate-file': switch (error) {
setErrorHandler(t('error-duplicate-file')); case 'duplicate-file':
break; return t('error-duplicate-file');
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: return t('error-uploading-file');
setErrorHandler(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;
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob) }, [selectedFiles]);
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
// но у него есть свои особенности и стандартные запросы не работают. const handleStartAllUploads = useCallback(async (): Promise<void> => {
const handleUnload = () => { const uploadNextBatch = async () => {
if (uploadId) { const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' }); const batch = currentPending.slice(0, maxParallelUploads);
//const blob = new Blob([data], { type: 'application/json' });
//navigator.sendBeacon('/api/v1/data/cancel', blob); if (batch.length === 0) return;
console.log('sendBeacon');
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();
} }
}; };
if (uploadId) {
window.addEventListener('unload', handleUnload);
}
return () => { await uploadNextBatch();
window.removeEventListener('unload', handleUnload); }, [maxParallelUploads]);
};
}, [uploadId]); 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)
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
// но у него есть свои особенности и стандартные запросы не работают.
const handleUnload = () => {
if (activeUploads.size) {
//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 (activeUploads.size) {
window.addEventListener('unload', handleUnload);
}
return () => {
window.removeEventListener('unload', handleUnload);
};
}, [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,115 +426,171 @@ 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" && (
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')} <p className="description" >
</p> {t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
)} </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" >
</div> <p className="text-red-600 font-medium" > {error} </p>
)} </div>
)
}
{selectedFile && ( {
<div selectedFiles.length > 0 && (
className={`selected-file ${isFileUploaded ? 'done' : ''}`} <div className="mt-6" >
> <div className="flex justify-between items-center mb-4" >
<div className="selected-file-file-info"> <div>
<div> <p className="text-gray-700 font-medium" >
<p className="text-gray-700"> {t('total-files')}: {selectedFiles.length} |
<span className="font-medium"> {t('uploading')}: {uploadingCount} |
{t('file')}: {t('completed')}: {completedCount}
</span> {selectedFile.name} </p>
</p> < div className="w-full bg-gray-200 rounded-full h-2.5 mt-2" >
<p className="text-gray-700"> <div
<span className="font-medium"> className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
{t('size')}: style={{ width: `${getTotalProgress}%` }
</span> {selectedFile.size} }
</p> > </div>
<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 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
src={file.preview}
alt="Предпросмотр загруженного изображения"
className="uploaded-image"
onError={() => setError('Не удалось загрузить превью изображения')
}
/>
)}
{
file.status === 'pending' && (
<div className="mt-3 text-center" >
<button
className="btn btn-confirm text-sm py-1 px-4"
onClick={() => uploadSingleFile(file)
}
type="button"
disabled={uploadingCount >= maxParallelUploads}
>
{t('start-upload')}
</button>
</div>
)}
</div>
))}
</div> </div>
</div> </div>
)}
{(fileType === 'image' && selectedFile.preview) && (
<img
src={selectedFile.preview}
alt="Предпросмотр загруженного изображения"
className="uploaded-image"
onError={() => setErrorHandler('Не удалось загрузить превью изображения')}
/>
)}
<div className="mt-4 flex gap-3 justify-center relative">
<button
className="btn btn-cancel"
onClick={handleClearFile}
type="button"
>
{isFileUploaded ? t('close') : t('cancel')}
</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>
); );
+8 -1
View File
@@ -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",
+9 -2
View File
@@ -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": "Такой реферальной ссылки не существует."
} }
} }