add local file serach in to header menu
This commit is contained in:
@@ -7,6 +7,7 @@ import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import BurgerMenu from '@/app/ui/navigation/burger-menu';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
import LocalFileCheck from '@/app/ui/header/local-file-check';
|
||||
|
||||
export default function HeaderPanel() {
|
||||
const t = useTranslations("Global");
|
||||
@@ -14,6 +15,7 @@ export default function HeaderPanel() {
|
||||
return (
|
||||
<header className="header">
|
||||
<BurgerMenu />
|
||||
<LocalFileCheck />
|
||||
<div className="header-action">
|
||||
{/* <LanguageSwitcher /> */}
|
||||
{typeof userData?.tariffInfo?.tokens === 'number' && (
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useMemo, ChangeEvent, useCallback } from 'react';
|
||||
import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { fileUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
|
||||
import { getFileType } from '@/app/lib/getFileType';
|
||||
import { searchUserFiles } from '@/app/actions/searchActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string,
|
||||
file_name: string,
|
||||
total_chunks: number,
|
||||
chunk_size: number,
|
||||
status: string
|
||||
}
|
||||
|
||||
export default function LocalFileCheck() {
|
||||
const { data: allFilesExtensions } = useAllFilesExtensions();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploadId, setUploadId] = useState<string | null>(null);
|
||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
const [fileId, setFileId] = useState<string | null>(null);
|
||||
const [searchResult, setSearchResult] = useState<string | null>(null);
|
||||
const isCancelledRef = useRef(false);
|
||||
|
||||
const childRef = useRef(null);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const allExtensions = useMemo(() => {
|
||||
if (allFilesExtensions) {
|
||||
return [...allFilesExtensions.images, ...allFilesExtensions.videos, ...allFilesExtensions.audios, ...allFilesExtensions.documents]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allExtensions || !Array.isArray(allExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allExtensions]);
|
||||
|
||||
const handleButtonClick = (): void => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
handleFileSelect(file);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||
if (!allExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allExtensions.includes(extension as string)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('unsupported-file-format')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SIZE = allFilesExtensions?.maxFileSize ? allFilesExtensions?.maxFileSize : 0;
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('the-file-is-too-large')
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
||||
setSearchResult(null);
|
||||
|
||||
if (!file) {
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileId) {
|
||||
await removeUserFile(fileId, 1);
|
||||
setFileId(null);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsFileUploaded(false);
|
||||
|
||||
if (childRef.current) {
|
||||
//@ts-ignore
|
||||
childRef.current.clearList();
|
||||
}
|
||||
|
||||
setUploadProgress(0);
|
||||
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(validation.errorMessage || t('unknown-validation-error'));
|
||||
return;
|
||||
}
|
||||
|
||||
handlerFileUpload({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
|
||||
}, [t]);
|
||||
|
||||
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
||||
if (uploadId) return;
|
||||
|
||||
setError(null);
|
||||
isCancelledRef.current = false;
|
||||
const file = fileInfo.file;
|
||||
const fileType = getFileType(fileInfo.file.name, allFilesExtensions);
|
||||
|
||||
try {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
setUploadId(response.upload_id);
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLastChunk = chunkIndex === totalChunks - 1;
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = 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);
|
||||
formData.append('findSimilar', '1');
|
||||
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.errorMesage !== null) {
|
||||
throw chunkResponse.errorMesage;
|
||||
}
|
||||
|
||||
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
|
||||
|
||||
if (isLastChunk && chunkResponse.fileId) {
|
||||
setFileId(chunkResponse.fileId);
|
||||
document.cookie = `searchedFileId=${chunkResponse.fileId}`
|
||||
handlerSearchUserFile(chunkResponse.fileId, fileInfo.name);
|
||||
}
|
||||
}
|
||||
|
||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setIsFileUploaded(true);
|
||||
} else {
|
||||
throw new Error('Not all chunks were uploaded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
setError(t('error-uploading-file'));
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, allFilesExtensions]);
|
||||
|
||||
|
||||
const handlerSearchUserFile = useCallback(async (fileId: string, fileName: string): Promise<void> => {
|
||||
|
||||
try {
|
||||
let result = await searchUserFiles(fileId);
|
||||
|
||||
if (result.errorMessage) {
|
||||
throw result.errorMessage
|
||||
}
|
||||
|
||||
if (result?.content?.length) {
|
||||
result.content.some((e: any) => {
|
||||
if (e.status === 'PROTECTED') {
|
||||
toast.success(t('file-file-name-already-exists', {
|
||||
fileName: fileName
|
||||
}));
|
||||
setSearchResult(t('file-file-name-already-exists', {
|
||||
fileName: fileName
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
toast.warning(t('file-file-name-does-not-exist', {
|
||||
fileName: fileName
|
||||
}));
|
||||
setSearchResult(t('file-file-name-does-not-exist', {
|
||||
fileName: fileName
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
if (error === 'file-for-search-unsupported') {
|
||||
toast.error(t(error));
|
||||
} else {
|
||||
toast.error(t('search-error'));
|
||||
}
|
||||
}
|
||||
|
||||
}, [fileId])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="local-file-check"
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
type="button"
|
||||
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
||||
style={{ minWidth: '140px' }}
|
||||
>
|
||||
<span>Проверка файла</span>
|
||||
{(uploadProgress !== 0 && uploadProgress !== 100) && (
|
||||
<div className="loading-animation">
|
||||
<span className="global-spinner"></span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="local-file-check-result"
|
||||
onClick={() => {
|
||||
setSearchResult(null);
|
||||
setIsFileUploaded(false);
|
||||
}}
|
||||
>
|
||||
{searchResult}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -196,24 +196,6 @@ export function FileSearchPanel(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="search-counter" id="search-counter">
|
||||
<div className="counter-info">
|
||||
<div className="counter-icon" id="counter-icon"></div>
|
||||
<div className="counter-text">
|
||||
<div className="counter-label">{t('global-searches-today')}</div>
|
||||
<div className="counter-value" id="counter-value">0 {t('out-of')} 10</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
id="progress-fill"
|
||||
style={{ width: '0%' }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className={`global-loading ${loading ? 'show' : ''}`} id="global-loading">
|
||||
<div className="global-spinner"></div>
|
||||
<div className="global-loading-text" id="global-loading-text">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
import { FileSearchPanel } from '@/app/ui/search/file-search-panel';
|
||||
import { getFileType } from '@/app/lib/getFileType';
|
||||
import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions';
|
||||
interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
@@ -33,10 +34,9 @@ export interface AllowedExtensions {
|
||||
|
||||
interface SectionSearchFile {
|
||||
allowedExtensions: AllowedExtensions
|
||||
maxFileSize: number
|
||||
}
|
||||
|
||||
export default function SectionSearchFile({ allowedExtensions, maxFileSize }: SectionSearchFile) {
|
||||
export default function SectionSearchFile() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||
@@ -53,7 +53,15 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const allExtensions = useMemo(() => { return [...allowedExtensions.images, ...allowedExtensions.videos, ...allowedExtensions.audios, ...allowedExtensions.documents] }, []);
|
||||
const { data: allFilesExtensions, isLoading, isError } = useAllFilesExtensions();
|
||||
|
||||
const allExtensions = useMemo(() => {
|
||||
if (allFilesExtensions) {
|
||||
return [...allFilesExtensions.images, ...allFilesExtensions.videos, ...allFilesExtensions.audios, ...allFilesExtensions.documents]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}, []);
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allExtensions || !Array.isArray(allExtensions)) {
|
||||
@@ -73,7 +81,7 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SIZE = maxFileSize;
|
||||
const MAX_SIZE = allFilesExtensions?.maxFileSize ? allFilesExtensions?.maxFileSize : 0;
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return {
|
||||
@@ -168,7 +176,7 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se
|
||||
setError(null);
|
||||
isCancelledRef.current = false;
|
||||
const file = fileInfo.file;
|
||||
const fileType = getFileType(fileInfo.file.name, allowedExtensions);
|
||||
const fileType = getFileType(fileInfo.file.name, allFilesExtensions);
|
||||
|
||||
try {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
@@ -238,7 +246,7 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, allowedExtensions]);
|
||||
}, [uploadId, allFilesExtensions]);
|
||||
|
||||
useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
@@ -351,7 +359,7 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se
|
||||
</div>
|
||||
|
||||
{isFileUploaded && (
|
||||
<FileSearchPanel fileId={fileId} ref={childRef} allowedExtensions={allowedExtensions} fileType={fileType} />
|
||||
<FileSearchPanel fileId={fileId} ref={childRef} allowedExtensions={allFilesExtensions} fileType={fileType} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user