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 ( +
+ + +
{ + 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": "и",