import { useState, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { Search as SearchIcon, ChevronLeft, ChevronRight, Download, X, Plus } from 'lucide-react' import { searchFurry, searchAnime, getFurryTags, getAnimeTags } from '../utils/api' import { hapticFeedback, getTelegramUser } from '../utils/telegram' import CreatePostModal from '../components/CreatePostModal' import api from '../utils/api' import './Search.css' export default function Search({ user }) { const initialMode = user.settings?.searchPreference === 'anime' ? 'anime' : 'furry' const [mode, setMode] = useState(initialMode) const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [loading, setLoading] = useState(false) const [tagSuggestions, setTagSuggestions] = useState([]) const [currentIndex, setCurrentIndex] = useState(0) const [showViewer, setShowViewer] = useState(false) const [selectedImages, setSelectedImages] = useState([]) const [selectionMode, setSelectionMode] = useState(false) const [showCreatePost, setShowCreatePost] = useState(false) const [imageForPost, setImageForPost] = useState(null) const touchStartX = useRef(0) const touchEndX = useRef(0) const isVideoUrl = (url = '') => { if (!url) return false const clean = url.split('?')[0].toLowerCase() return clean.endsWith('.mp4') || clean.endsWith('.webm') || clean.endsWith('.mov') || clean.endsWith('.m4v') } useEffect(() => { if (query.length > 1) { loadTagSuggestions() } else { setTagSuggestions([]) } }, [query, mode]) const loadTagSuggestions = async () => { try { // Разбить query по пробелам и взять последний тег для автокомплита const queryParts = query.trim().split(/\s+/) const lastTag = queryParts[queryParts.length - 1] || query.trim() // Если нет текста для поиска, не загружаем предложения if (!lastTag || lastTag.length < 1) { setTagSuggestions([]) return } let tags = [] if (mode === 'furry') { try { const furryTags = await getFurryTags(lastTag) if (furryTags && Array.isArray(furryTags)) { tags = [...tags, ...furryTags.map(t => ({ ...t, source: 'e621' }))] } } catch (error) { console.error('Ошибка загрузки e621 тегов:', error) // Продолжаем даже если e621 не работает } } if (mode === 'anime') { try { const animeTags = await getAnimeTags(lastTag) if (animeTags && Array.isArray(animeTags)) { tags = [...tags, ...animeTags.map(t => ({ ...t, source: 'gelbooru' }))] } } catch (error) { console.error('Ошибка загрузки Gelbooru тегов:', error) // Продолжаем даже если Gelbooru не работает } } // Убрать дубликаты const uniqueTags = tags.reduce((acc, tag) => { if (!acc.find(t => t.name === tag.name)) { acc.push(tag) } return acc }, []) setTagSuggestions(uniqueTags.slice(0, 10)) } catch (error) { console.error('Ошибка загрузки тегов:', error) setTagSuggestions([]) } } const handleSearch = async (searchQuery = query) => { if (!searchQuery.trim()) return try { setLoading(true) hapticFeedback('light') setResults([]) let allResults = [] if (mode === 'furry') { try { const furryResults = await searchFurry(searchQuery, { limit: 320, page: 1 }) if (Array.isArray(furryResults)) { allResults = [...allResults, ...furryResults] } } catch (error) { console.error('Ошибка e621 поиска:', error) } } if (mode === 'anime') { try { const animeResults = await searchAnime(searchQuery, { limit: 320, page: 1 }) if (Array.isArray(animeResults)) { allResults = [...allResults, ...animeResults] } } catch (error) { console.error('Ошибка Gelbooru поиска:', error) } } setResults(allResults) setTagSuggestions([]) if (allResults.length > 0) { hapticFeedback('success') } else { hapticFeedback('error') } } catch (error) { console.error('Ошибка поиска:', error) hapticFeedback('error') setResults([]) } finally { setLoading(false) } } const handleTagClick = (tagName) => { // Разбить текущий query по пробелам const queryParts = query.trim().split(/\s+/) // Убрать последний тег (если он есть) и добавить новый const existingTags = queryParts.slice(0, -1).filter(t => t.trim()) const newQuery = existingTags.length > 0 ? [...existingTags, tagName].join(' ') : tagName setQuery(newQuery) handleSearch(newQuery) } const openViewer = (index) => { if (selectionMode) { toggleImageSelection(index) } else { setCurrentIndex(index) setShowViewer(true) hapticFeedback('light') } } const toggleImageSelection = (index) => { const imageId = `${results[index].source}-${results[index].id}` if (selectedImages.includes(imageId)) { setSelectedImages(selectedImages.filter(id => id !== imageId)) } else { setSelectedImages([...selectedImages, imageId]) } hapticFeedback('light') } const toggleSelectionMode = () => { setSelectionMode(!selectionMode) setSelectedImages([]) hapticFeedback('light') } const handleSendSelected = async () => { if (selectedImages.length === 0) return try { hapticFeedback('light') const telegramUser = getTelegramUser() if (telegramUser) { // Найти выбранные изображения const selectedPhotos = results.filter((img, index) => { const imageId = `${img.source}-${img.id}` return selectedImages.includes(imageId) }) const photos = selectedPhotos.map(img => ({ url: img.url, caption: `${img.source} - ${img.id}` })) await api.post('/bot/send-photos', { userId: telegramUser.id, photos: photos }) hapticFeedback('success') alert(`✅ ${selectedImages.length} изображений отправлено в ваш Telegram!`) setSelectedImages([]) setSelectionMode(false) } else { alert('Функция доступна только в Telegram') } } catch (error) { console.error('Ошибка:', error) hapticFeedback('error') alert('Ошибка отправки') } } const handleNext = () => { if (currentIndex < results.length - 1) { setCurrentIndex(currentIndex + 1) hapticFeedback('light') } } const handlePrev = () => { if (currentIndex > 0) { setCurrentIndex(currentIndex - 1) hapticFeedback('light') } } const handleTouchStart = (e) => { touchStartX.current = e.touches[0].clientX } const handleTouchMove = (e) => { touchEndX.current = e.touches[0].clientX } const handleTouchEnd = () => { const diff = touchStartX.current - touchEndX.current const threshold = 50 // минимальное расстояние для свайпа if (Math.abs(diff) > threshold) { if (diff > 0) { // Свайп влево - следующая картинка handleNext() } else { // Свайп вправо - предыдущая картинка handlePrev() } } } const handleKeyDown = (e) => { if (e.key === 'ArrowLeft') { handlePrev() } else if (e.key === 'ArrowRight') { handleNext() } else if (e.key === 'Escape') { setShowViewer(false) } } useEffect(() => { if (showViewer) { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) } }, [showViewer, currentIndex]) const handleDownload = async () => { const currentImage = results[currentIndex] if (!currentImage) return try { hapticFeedback('light') const telegramUser = getTelegramUser() if (telegramUser) { // Отправить через backend в ЛС с ботом const caption = `${currentImage.source} - ID: ${currentImage.id}\nТеги: ${currentImage.tags.slice(0, 3).join(', ')}` await api.post('/bot/send-photo', { userId: telegramUser.id, photoUrl: currentImage.url, caption: caption }) hapticFeedback('success') alert('✅ Изображение отправлено в ваш Telegram!') } else { // Fallback - обычное скачивание const response = await fetch(currentImage.url) const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `nakama-${currentImage.id}.jpg` document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) hapticFeedback('success') } } catch (error) { console.error('Ошибка:', error) hapticFeedback('error') alert('Ошибка отправки. Проверьте настройки бота.') } } const handleCreatePost = () => { const currentImage = results[currentIndex] setImageForPost(currentImage.url) setShowViewer(false) setShowCreatePost(true) hapticFeedback('light') } const handlePostCreated = (newPost) => { setShowCreatePost(false) setImageForPost(null) hapticFeedback('success') alert('✅ Пост создан!') } return (
{/* Хедер */}

Поиск

{results.length > 0 && ( )}
{/* Режимы поиска */}
{/* Строка поиска */}
setQuery(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleSearch()} /> {query && ( )}
{/* Подсказки тегов */} {tagSuggestions.length > 0 && (
{tagSuggestions.map((tag, index) => ( ))}
)}
{/* Результаты */}
{loading ? (

Поиск...

) : results.length === 0 && query ? (

Ничего не найдено

Попробуйте другие теги
) : results.length === 0 ? (

Введите теги для поиска

Используйте e621 и gelbooru
) : ( <>
{results.map((item, index) => { const imageId = `${item.source}-${item.id}` const isSelected = selectedImages.includes(imageId) return (
openViewer(index)} > {`Result
{item.source} {item.rating}
{selectionMode && (
{isSelected && }
)}
) })}
{/* Кнопка загрузки дополнительных результатов */} {/* Кнопка отправки выбранных */} {selectionMode && selectedImages.length > 0 && (
)} )}
{/* Просмотрщик изображений через Portal */} {showViewer && results[currentIndex] && createPortal(
e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} >
{currentIndex + 1} / {results.length}
e.stopPropagation()} > {isVideoUrl(results[currentIndex].url) ? (
{results[currentIndex].tags.slice(0, 5).map((tag, i) => ( {tag} ))}
Score: {results[currentIndex].score} Source: {results[currentIndex].source}
, document.body )} {/* Модалка создания поста */} {showCreatePost && ( setShowCreatePost(false)} onPostCreated={() => { setShowCreatePost(false) setShowViewer(false) }} initialImage={results[currentIndex]?.url} /> )}
) }