add local file serach in to header menu

This commit is contained in:
smanylov
2026-03-13 17:29:52 +07:00
parent 8c52839c02
commit 00c4f4062d
11 changed files with 395 additions and 38 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "no-copy-frontend",
"version": "0.59.0",
"version": "0.60.0",
"private": true,
"scripts": {
"dev": "next dev -p 2999",
+1 -10
View File
@@ -21,16 +21,7 @@ export default async function Page() {
</p>
</div>
<div className="search-grid">
<SectionSearchFile
maxFileSize={max_file_size}
/* allowedExtensions={allExtensions} */
allowedExtensions={{
images: extensionImage,
videos: extensionVideo,
audios: extensionAudio,
documents: extensionDocument
}}
/>
<SectionSearchFile />
<div className="search-sidebar">
<SupportedFormats
extensionVideo={extensionVideo}
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { getAllFilesExtensions } from '@/app/lib/prefetch-queries';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
const EMPTY_EXTENSIONS: allFilesExtensions = {
videos: [],
audios: [],
images: [],
documents: [],
maxFileSize: 0
};
export const useAllFilesExtensions = () => {
const query = useQuery({
queryKey: ['allFilesExtensions'],
queryFn: getAllFilesExtensions,
retry: false,
initialData: EMPTY_EXTENSIONS
});
return {
...query,
data: query.data ?? EMPTY_EXTENSIONS
};
};
+33
View File
@@ -2,6 +2,34 @@ import { QueryClient } from '@tanstack/react-query';
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
import { getUserFilesData } from '@/app/actions/fileEntity';
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
export async function getAllFilesExtensions(): Promise<allFilesExtensions> {
const [video, audio, image, document] = await Promise.all([
getAllowedFilesExtensions('video'),
getAllowedFilesExtensions('audio'),
getAllowedFilesExtensions('image'),
getAllowedFilesExtensions('document')
]);
const result: allFilesExtensions = {
videos: video?.file_extension ? video.file_extension : [],
audios: audio?.file_extension ? audio.file_extension : [],
images: image?.file_extension ? image.file_extension : [],
documents: document?.file_extension ? document.file_extension : [],
maxFileSize: video?.max_file_size ? video.max_file_size : 0
}
return result;
}
export async function prefetchLayoutQueries(queryClient: QueryClient) {
await Promise.all([
@@ -35,5 +63,10 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
}
}
}),
queryClient.prefetchQuery({
queryKey: ['allFilesExtensions'],
queryFn: () => getAllFilesExtensions(),
staleTime: 5 * 60 * 1000
}),
]);
}
+12
View File
@@ -4689,4 +4689,16 @@
&-item {
text-align: center;
}
}
.local-file-check {
margin-right: auto;
position: relative;
display: flex;
gap: 5px;
align-items: center;
.local-file-check-result {
cursor: pointer;
}
}
+2
View File
@@ -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' && (
+292
View File
@@ -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>
)
}
-18
View File
@@ -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">
+15 -7
View File
@@ -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>
);
+3 -1
View File
@@ -301,7 +301,9 @@
"purchasing-tokens": "Purchasing tokens",
"payment-of-the-tariff": "Payment of the tariff",
"file-verification-started-successfully": "File verification started successfully.",
"an-error-occurred-while-starting-file-verification": "An error occurred while starting file verification."
"an-error-occurred-while-starting-file-verification": "An error occurred while starting file verification.",
"file-file-name-already-exists": "The file {fileName} already exists on the account.",
"file-file-name-does-not-exist": "The file {fileName} does not exist on the account."
},
"Login-register-form": {
"and": "and",
+3 -1
View File
@@ -301,7 +301,9 @@
"purchasing-tokens": "Покупка токенов",
"payment-of-the-tariff": "Оплата тарифа",
"file-verification-started-successfully": "Проверка файлов успешно запущена.",
"an-error-occurred-while-starting-file-verification": "При запуске проверки файлов произошла ошибка."
"an-error-occurred-while-starting-file-verification": "При запуске проверки файлов произошла ошибка.",
"file-file-name-already-exists": "Файл {fileName} уже есть на аккаунте.",
"file-file-name-does-not-exist": "Файла {fileName} на аккаунте нету."
},
"Login-register-form": {
"and": "и",