add search setting interface

This commit is contained in:
smanylov
2026-03-24 18:14:08 +07:00
parent 0955d66931
commit 18e700bb6e
12 changed files with 301 additions and 49 deletions
+3 -21
View File
@@ -1,36 +1,18 @@
import SectionSearchFile from '@/app/ui/search/section-search-file';
import { SupportedFormats } from '@/app/ui/search/supported-formats';
import { SearchStats } from '@/app/ui/search/search-stats';
import { RecentSearches } from '@/app/ui/search/recent-searches';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import SearchTitle from '@/app/ui/search/search-title';
export default async function Page() {
const { file_extension: extensionVideo, max_file_size } = await getAllowedFilesExtensions('video');
const { file_extension: extensionAudio } = await getAllowedFilesExtensions('audio');
const { file_extension: extensionImage } = await getAllowedFilesExtensions('image');
const { file_extension: extensionDocument } = await getAllowedFilesExtensions('document');
return (
<>
<div className="block-wrapper">
<h1 className="page-title">
Поиск защищенного контента
</h1>
<p className="page-description">
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
</p>
</div>
<SearchTitle />
<div className="search-grid">
<SectionSearchFile />
<div className="search-sidebar">
<SupportedFormats
extensionVideo={extensionVideo}
extensionAudio={extensionAudio}
extensionImage={extensionImage}
extensionDocument={extensionDocument}
/>
<SupportedFormats />
<SearchStats />
{/* <RecentSearches /> */}
</div>
</div>
</>
+63
View File
@@ -0,0 +1,63 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function getSearchSettings() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_body) {
return parsed.message_body;
}
} else {
return null;
}
} catch (error) {
return null;
}
}
export async function putSearchSettings(proxy: boolean, yandex: boolean, google: boolean) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
engines: {
yandex: yandex,
google: google
},
proxyEnabled: proxy
})
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_desc === 'Settings updated') {
return true;
}
} else {
return false;
}
} catch (error) {
return false;
}
}
@@ -2,10 +2,10 @@ import { useQuery } from '@tanstack/react-query';
import { fetchViolationStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic {
total_violations: 2565,
new_violations: 2565,
in_progress_violations: 0,
resolved_violations: 0
total_violations: number,
new_violations: number,
in_progress_violations: number,
resolved_violations: number
}
export const useViolationStatistic = () => {
+45
View File
@@ -737,3 +737,48 @@
/* transform: translateY(-2px); */
}
}
.search-settings {
&-wrapper {
margin-bottom: 20px;
background: #f0f9ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
margin-bottom: 20px;
padding: 16px;
color: #1e40af;
}
&-header {
display: flex;
gap: 10px;
font-weight: 600;
&.loading {
opacity: 0.5;
}
}
&-item {
cursor: pointer;
span {
margin-left: 4px;
color: v.$red;
&.active {
color: v.$green;
}
}
&:hover {
text-decoration: underline;
}
}
/* &-action-panel {
display: flex;
gap: 10px;
} */
}
+1 -1
View File
@@ -15,7 +15,7 @@ export default function HeaderPanel() {
return (
<header className="header">
<BurgerMenu />
<LocalFileCheck />
{/* <LocalFileCheck /> */}
<div className="header-action">
{/* <LanguageSwitcher /> */}
{typeof userData?.tariffInfo?.tokens === 'number' && (
+132
View File
@@ -0,0 +1,132 @@
'use client'
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { getSearchSettings, putSearchSettings } from '@/app/actions/SearchSetting';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
interface SearchSettings {
engines: {
yandex: boolean;
google: boolean;
};
proxyEnabled: boolean;
}
export default function SearchSettingsBlock() {
const { data: searchStats } = useQuery<SearchSettings>({
queryKey: ['searchStats'],
queryFn: getSearchSettings,
retry: false
});
const queryClient = useQueryClient();
const t = useTranslations('Global');
const [isLoading, setIsLoading] = useState(false);
async function changeHandler(changetItem: 'proxy' | 'yandex' | 'google') {
if (!searchStats) {
toast.error(t('error'));
return;
}
if (isLoading) {
return;
}
setIsLoading(true);
let proxy = searchStats.proxyEnabled;
let yandex = searchStats.engines.yandex;
let google = searchStats.engines.google;
switch (changetItem) {
case 'yandex':
yandex = !yandex;
break
case 'google':
google = !google;
break
case 'proxy':
proxy = !proxy;
break
}
try {
const response = await putSearchSettings(proxy, yandex, google);
if (response) {
toast.success('Succes');
await queryClient.invalidateQueries({ queryKey: ['searchStats'] });
} else {
toast.warning('Not succes');
}
} catch (error) {
toast.error(t('error'));
} finally {
setIsLoading(false);
}
}
return (
<div
className="search-settings-wrapper"
>
<div
className={`search-settings-header ${isLoading ? 'loading' : ''}`}
>
<div
className={`search-settings-item`}
onClick={() => {
changeHandler('proxy');
}}
>
{t('proxy-enabled')}:
<span className={`${searchStats?.proxyEnabled ? 'active' : ''}`} >
{searchStats?.proxyEnabled ? t('yes') : t('no')}
</span>
</div>
<div
className={`search-settings-item`}
onClick={() => {
changeHandler('yandex');
}}
>
yandex:
<span
className={`${searchStats?.engines?.yandex ? 'active' : ''}`}
>
{searchStats?.engines?.yandex ? t('yes') : t('no')}
</span>
</div>
<div
className={`search-settings-item`}
onClick={() => {
changeHandler('google');
}}
>
google:
<span
className={`${searchStats?.engines?.google ? 'active' : ''}`}
>
{searchStats?.engines?.google ? t('yes') : t('no')}
</span>
</div>
{isLoading && (
<div
className="relative ml-2.5"
>
<div className="loading-animation">
<div className="global-spinner"></div>
</div>
</div>
)}
</div>
</div>
)
}
+1 -5
View File
@@ -9,14 +9,10 @@ export function SearchStats() {
const { data: userSearchData, isLoading, isError, error } = useUserSearchData();
const t = useTranslations('Global');
useEffect(() => {
console.log(userSearchData);
}, [userSearchData])
return (
<div className="stats-search">
<div className="stats-title">
Статистика поиска
{t('search-stats')}
</div>
<div className="stat-item">
+18
View File
@@ -0,0 +1,18 @@
'use client'
import { useTranslations } from 'next-intl'
export default function SearchTitle() {
const t = useTranslations('Global');
return (
<div className="block-wrapper">
<h1 className="page-title">
{t('search-protected-content')}
</h1>
<p className="page-description">
{t('search-title-description')}
</p>
</div>
)
}
+7 -3
View File
@@ -10,6 +10,7 @@ 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';
import SearchSettingsBlock from '@/app/ui/search/search-settings-block';
interface SelectedFile {
file: File;
preview: string | undefined;
@@ -287,13 +288,16 @@ export default function SectionSearchFile() {
return (
<div className="upload-section">
<div className="search-info">
<div className="search-info-title">Как работает поиск?</div>
<div className="search-info-title">
{t('how-does-search-work')}
</div>
<div className="search-info-text">
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
используя алгоритмы компьютерного зрения и цифровых отпечатков.
{t('search-info-description-text')}
</div>
</div>
<SearchSettingsBlock />
<div
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
+11 -13
View File
@@ -1,20 +1,18 @@
'use client'
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl';
import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions';
interface supportedFormats {
extensionVideo: string[]
extensionAudio: string[]
extensionImage: string[]
extensionDocument: string[]
}
export function SupportedFormats({ extensionVideo, extensionAudio, extensionImage, extensionDocument }: supportedFormats) {
export function SupportedFormats() {
const t = useTranslations('Global');
const { data: allFilesExtensions, isLoading, isError } = useAllFilesExtensions();
return (
<div className="supported-formats">
<div className="formats-title">
Поддерживаемые форматы
{t('supported-formats')}
</div>
<div className="format-group">
@@ -22,7 +20,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconImageFile /> {t('images')}
</div>
<div className="format-list">
{extensionImage.join(', ')}
{allFilesExtensions?.images?.join(', ')}
</div>
</div>
@@ -31,7 +29,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconVideoFile /> {t('videos')}
</div>
<div className="format-list">
{extensionVideo.join(', ')}
{allFilesExtensions?.videos?.join(', ')}
</div>
</div>
@@ -40,7 +38,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconAudioFile /> {t('audios')}
</div>
<div className="format-list">
{extensionAudio.join(', ')}
{allFilesExtensions?.audios?.join(', ')}
</div>
</div>
@@ -49,7 +47,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconDocument /> {t('documents')}
</div>
<div className="format-list">
{extensionDocument.join(', ')}
{allFilesExtensions?.documents?.join(', ')}
</div>
</div>
</div>
+8 -1
View File
@@ -358,7 +358,14 @@
"payment-history": "Payment history",
"file-verification": "File verification",
"file-is-protected": "The file is protected.",
"read-more": "Read more"
"read-more": "Read more",
"proxy-enabled": "Proxy enabled",
"supported-formats": "Supported formats",
"search-stats": "Search stats",
"search-protected-content": "Search for protected content",
"search-title-description": "Upload a file to search for similar protected content in your library and on the web",
"how-does-search-work": "How does search work?",
"search-info-description-text": "Our system analyzes the uploaded file and compares it with your protected files, using computer vision and digital fingerprinting algorithms."
},
"Login-register-form": {
"and": "and",
+8 -1
View File
@@ -358,7 +358,14 @@
"payment-history": "История выплат",
"file-verification": "Проверка файла",
"file-is-protected": "Файл защищен.",
"read-more": "Подробнее"
"read-more": "Подробнее",
"proxy-enabled": "Прокси включен",
"supported-formats": "Поддерживаемые форматы",
"search-stats": "Статистика поиска",
"search-protected-content": "Поиск защищенного контента",
"search-title-description": "Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете",
"how-does-search-work": "Как работает поиск?",
"search-info-description-text": "Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами, используя алгоритмы компьютерного зрения и цифровых отпечатков."
},
"Login-register-form": {
"and": "и",