From 00c4f4062d261831f53c4fd43c64a95b99892b18 Mon Sep 17 00:00:00 2001
From: smanylov
Date: Fri, 13 Mar 2026 17:29:52 +0700
Subject: [PATCH] add local file serach in to header menu
---
package.json | 2 +-
src/app/[locale]/pages/search/page.tsx | 11 +-
.../react-query/useAllFilesExtensions.ts | 33 ++
src/app/lib/prefetch-queries.ts | 33 ++
src/app/styles/pages-styles.scss | 12 +
src/app/ui/header/headerPanel.tsx | 2 +
src/app/ui/header/local-file-check.tsx | 292 ++++++++++++++++++
src/app/ui/search/file-search-panel.tsx | 18 --
src/app/ui/search/section-search-file.tsx | 22 +-
src/i18n/messages/en.json | 4 +-
src/i18n/messages/ru.json | 4 +-
11 files changed, 395 insertions(+), 38 deletions(-)
create mode 100644 src/app/hooks/react-query/useAllFilesExtensions.ts
create mode 100644 src/app/ui/header/local-file-check.tsx
diff --git a/package.json b/package.json
index 13fd357..876d538 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "no-copy-frontend",
- "version": "0.59.0",
+ "version": "0.60.0",
"private": true,
"scripts": {
"dev": "next dev -p 2999",
diff --git a/src/app/[locale]/pages/search/page.tsx b/src/app/[locale]/pages/search/page.tsx
index bae66e5..9541dab 100644
--- a/src/app/[locale]/pages/search/page.tsx
+++ b/src/app/[locale]/pages/search/page.tsx
@@ -21,16 +21,7 @@ export default async function Page() {
-
+
{
+ const query = useQuery({
+ queryKey: ['allFilesExtensions'],
+ queryFn: getAllFilesExtensions,
+ retry: false,
+ initialData: EMPTY_EXTENSIONS
+ });
+
+ return {
+ ...query,
+ data: query.data ?? EMPTY_EXTENSIONS
+ };
+};
\ No newline at end of file
diff --git a/src/app/lib/prefetch-queries.ts b/src/app/lib/prefetch-queries.ts
index 71c5d57..d26228c 100644
--- a/src/app/lib/prefetch-queries.ts
+++ b/src/app/lib/prefetch-queries.ts
@@ -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 {
+ 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
+ }),
]);
}
\ No newline at end of file
diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss
index a665a8b..2e68a22 100644
--- a/src/app/styles/pages-styles.scss
+++ b/src/app/styles/pages-styles.scss
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/src/app/ui/header/headerPanel.tsx b/src/app/ui/header/headerPanel.tsx
index bca4a72..83b97be 100644
--- a/src/app/ui/header/headerPanel.tsx
+++ b/src/app/ui/header/headerPanel.tsx
@@ -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 (
+
{/*
*/}
{typeof userData?.tariffInfo?.tokens === 'number' && (
diff --git a/src/app/ui/header/local-file-check.tsx b/src/app/ui/header/local-file-check.tsx
new file mode 100644
index 0000000..91d371e
--- /dev/null
+++ b/src/app/ui/header/local-file-check.tsx
@@ -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
(null);
+ const [error, setError] = useState(null);
+ const [uploadId, setUploadId] = useState(null);
+ const [isFileUploaded, setIsFileUploaded] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [fileId, setFileId] = useState(null);
+ const [searchResult, setSearchResult] = useState(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): 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 => {
+ 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 => {
+ 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 => {
+
+ 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 (
+
+
+
+ Проверка файла
+ {(uploadProgress !== 0 && uploadProgress !== 100) && (
+
+
+
+ )}
+
+
{
+ setSearchResult(null);
+ setIsFileUploaded(false);
+ }}
+ >
+ {searchResult}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/ui/search/file-search-panel.tsx b/src/app/ui/search/file-search-panel.tsx
index a48f5b8..1b2bf7b 100644
--- a/src/app/ui/search/file-search-panel.tsx
+++ b/src/app/ui/search/file-search-panel.tsx
@@ -196,24 +196,6 @@ export function FileSearchPanel(
- {/*
-
-
-
-
{t('global-searches-today')}
-
0 {t('out-of')} 10
-
-
-
-
*/}
-
diff --git a/src/app/ui/search/section-search-file.tsx b/src/app/ui/search/section-search-file.tsx
index 6c2e52f..d4fe806 100644
--- a/src/app/ui/search/section-search-file.tsx
+++ b/src/app/ui/search/section-search-file.tsx
@@ -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(null);
const [isDragging, setIsDragging] = useState(false);
const [selectedFile, setSelectedFile] = useState(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
{isFileUploaded && (
-
+
)}
);
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index e70496a..2c9800e 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -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",
diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json
index e9d4fdb..0376eab 100644
--- a/src/i18n/messages/ru.json
+++ b/src/i18n/messages/ru.json
@@ -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": "и",