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 SectionSearchFile from '@/app/ui/search/section-search-file';
import { SupportedFormats } from '@/app/ui/search/supported-formats'; import { SupportedFormats } from '@/app/ui/search/supported-formats';
import { SearchStats } from '@/app/ui/search/search-stats'; import { SearchStats } from '@/app/ui/search/search-stats';
import { RecentSearches } from '@/app/ui/search/recent-searches'; import SearchTitle from '@/app/ui/search/search-title';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export default async function Page() { 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 ( return (
<> <>
<div className="block-wrapper"> <SearchTitle />
<h1 className="page-title">
Поиск защищенного контента
</h1>
<p className="page-description">
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
</p>
</div>
<div className="search-grid"> <div className="search-grid">
<SectionSearchFile /> <SectionSearchFile />
<div className="search-sidebar"> <div className="search-sidebar">
<SupportedFormats <SupportedFormats />
extensionVideo={extensionVideo}
extensionAudio={extensionAudio}
extensionImage={extensionImage}
extensionDocument={extensionDocument}
/>
<SearchStats /> <SearchStats />
{/* <RecentSearches /> */}
</div> </div>
</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'; import { fetchViolationStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic { export interface UseViolationStatistic {
total_violations: 2565, total_violations: number,
new_violations: 2565, new_violations: number,
in_progress_violations: 0, in_progress_violations: number,
resolved_violations: 0 resolved_violations: number
} }
export const useViolationStatistic = () => { export const useViolationStatistic = () => {
+45
View File
@@ -737,3 +737,48 @@
/* transform: translateY(-2px); */ /* 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 ( return (
<header className="header"> <header className="header">
<BurgerMenu /> <BurgerMenu />
<LocalFileCheck /> {/* <LocalFileCheck /> */}
<div className="header-action"> <div className="header-action">
{/* <LanguageSwitcher /> */} {/* <LanguageSwitcher /> */}
{typeof userData?.tariffInfo?.tokens === 'number' && ( {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 { data: userSearchData, isLoading, isError, error } = useUserSearchData();
const t = useTranslations('Global'); const t = useTranslations('Global');
useEffect(() => {
console.log(userSearchData);
}, [userSearchData])
return ( return (
<div className="stats-search"> <div className="stats-search">
<div className="stats-title"> <div className="stats-title">
Статистика поиска {t('search-stats')}
</div> </div>
<div className="stat-item"> <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 { FileSearchPanel } from '@/app/ui/search/file-search-panel';
import { getFileType } from '@/app/lib/getFileType'; import { getFileType } from '@/app/lib/getFileType';
import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions'; import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions';
import SearchSettingsBlock from '@/app/ui/search/search-settings-block';
interface SelectedFile { interface SelectedFile {
file: File; file: File;
preview: string | undefined; preview: string | undefined;
@@ -287,13 +288,16 @@ export default function SectionSearchFile() {
return ( return (
<div className="upload-section"> <div className="upload-section">
<div className="search-info"> <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"> <div className="search-info-text">
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами, {t('search-info-description-text')}
используя алгоритмы компьютерного зрения и цифровых отпечатков.
</div> </div>
</div> </div>
<SearchSettingsBlock />
<div <div
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`} className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
+11 -13
View File
@@ -1,20 +1,18 @@
'use client'
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons'; import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useAllFilesExtensions } from '@/app/hooks/react-query/useAllFilesExtensions';
interface supportedFormats { export function SupportedFormats() {
extensionVideo: string[]
extensionAudio: string[]
extensionImage: string[]
extensionDocument: string[]
}
export function SupportedFormats({ extensionVideo, extensionAudio, extensionImage, extensionDocument }: supportedFormats) {
const t = useTranslations('Global'); const t = useTranslations('Global');
const { data: allFilesExtensions, isLoading, isError } = useAllFilesExtensions();
return ( return (
<div className="supported-formats"> <div className="supported-formats">
<div className="formats-title"> <div className="formats-title">
Поддерживаемые форматы {t('supported-formats')}
</div> </div>
<div className="format-group"> <div className="format-group">
@@ -22,7 +20,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconImageFile /> {t('images')} <IconImageFile /> {t('images')}
</div> </div>
<div className="format-list"> <div className="format-list">
{extensionImage.join(', ')} {allFilesExtensions?.images?.join(', ')}
</div> </div>
</div> </div>
@@ -31,7 +29,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconVideoFile /> {t('videos')} <IconVideoFile /> {t('videos')}
</div> </div>
<div className="format-list"> <div className="format-list">
{extensionVideo.join(', ')} {allFilesExtensions?.videos?.join(', ')}
</div> </div>
</div> </div>
@@ -40,7 +38,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconAudioFile /> {t('audios')} <IconAudioFile /> {t('audios')}
</div> </div>
<div className="format-list"> <div className="format-list">
{extensionAudio.join(', ')} {allFilesExtensions?.audios?.join(', ')}
</div> </div>
</div> </div>
@@ -49,7 +47,7 @@ export function SupportedFormats({ extensionVideo, extensionAudio, extensionImag
<IconDocument /> {t('documents')} <IconDocument /> {t('documents')}
</div> </div>
<div className="format-list"> <div className="format-list">
{extensionDocument.join(', ')} {allFilesExtensions?.documents?.join(', ')}
</div> </div>
</div> </div>
</div> </div>
+8 -1
View File
@@ -358,7 +358,14 @@
"payment-history": "Payment history", "payment-history": "Payment history",
"file-verification": "File verification", "file-verification": "File verification",
"file-is-protected": "The file is protected.", "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": { "Login-register-form": {
"and": "and", "and": "and",
+8 -1
View File
@@ -358,7 +358,14 @@
"payment-history": "История выплат", "payment-history": "История выплат",
"file-verification": "Проверка файла", "file-verification": "Проверка файла",
"file-is-protected": "Файл защищен.", "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": { "Login-register-form": {
"and": "и", "and": "и",