nakama/frontend/src/pages/Search.jsx

295 lines
9.1 KiB
React
Raw Normal View History

2025-11-03 20:35:01 +00:00
import { useState, useEffect } from 'react'
import { Search as SearchIcon, ChevronLeft, ChevronRight, Download, X } from 'lucide-react'
import { searchFurry, searchAnime, getFurryTags, getAnimeTags } from '../utils/api'
import { hapticFeedback } from '../utils/telegram'
import './Search.css'
export default function Search({ user }) {
const [mode, setMode] = useState(user.settings?.searchPreference || 'mixed')
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)
useEffect(() => {
if (query.length > 1) {
loadTagSuggestions()
} else {
setTagSuggestions([])
}
}, [query, mode])
const loadTagSuggestions = async () => {
try {
let tags = []
if (mode === 'furry' || mode === 'mixed') {
const furryTags = await getFurryTags(query)
tags = [...tags, ...furryTags.map(t => ({ ...t, source: 'e621' }))]
}
if (mode === 'anime' || mode === 'mixed') {
const animeTags = await getAnimeTags(query)
tags = [...tags, ...animeTags.map(t => ({ ...t, source: '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)
}
}
const handleSearch = async (searchQuery = query) => {
if (!searchQuery.trim()) return
try {
setLoading(true)
hapticFeedback('light')
setResults([])
let allResults = []
if (mode === 'furry' || mode === 'mixed') {
const furryResults = await searchFurry(searchQuery, { limit: 30 })
allResults = [...allResults, ...furryResults]
}
if (mode === 'anime' || mode === 'mixed') {
const animeResults = await searchAnime(searchQuery, { limit: 30 })
allResults = [...allResults, ...animeResults]
}
// Перемешать результаты если mixed режим
if (mode === 'mixed') {
allResults = allResults.sort(() => Math.random() - 0.5)
}
setResults(allResults)
setTagSuggestions([])
if (allResults.length > 0) {
hapticFeedback('success')
}
} catch (error) {
console.error('Ошибка поиска:', error)
hapticFeedback('error')
} finally {
setLoading(false)
}
}
const handleTagClick = (tagName) => {
setQuery(tagName)
handleSearch(tagName)
}
const openViewer = (index) => {
setCurrentIndex(index)
setShowViewer(true)
hapticFeedback('light')
}
const handleNext = () => {
if (currentIndex < results.length - 1) {
setCurrentIndex(currentIndex + 1)
hapticFeedback('light')
}
}
const handlePrev = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)
hapticFeedback('light')
}
}
const handleDownload = async () => {
const currentImage = results[currentIndex]
if (!currentImage) return
try {
hapticFeedback('light')
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')
}
}
return (
<div className="search-page">
{/* Хедер */}
<div className="search-header">
<h1>Поиск</h1>
</div>
{/* Режимы поиска */}
<div className="search-modes">
<button
className={`mode-btn ${mode === 'furry' ? 'active' : ''}`}
onClick={() => setMode('furry')}
style={{ color: mode === 'furry' ? 'var(--tag-furry)' : undefined }}
>
Furry
</button>
<button
className={`mode-btn ${mode === 'anime' ? 'active' : ''}`}
onClick={() => setMode('anime')}
style={{ color: mode === 'anime' ? 'var(--tag-anime)' : undefined }}
>
Anime
</button>
<button
className={`mode-btn ${mode === 'mixed' ? 'active' : ''}`}
onClick={() => setMode('mixed')}
>
Mixed
</button>
</div>
{/* Строка поиска */}
<div className="search-container">
<div className="search-input-wrapper">
<SearchIcon size={20} className="search-icon" />
<input
type="text"
placeholder="Поиск по тегам..."
value={query}
onChange={e => setQuery(e.target.value)}
onKeyPress={e => e.key === 'Enter' && handleSearch()}
/>
{query && (
<button className="clear-btn" onClick={() => setQuery('')}>
<X size={18} />
</button>
)}
</div>
{/* Подсказки тегов */}
{tagSuggestions.length > 0 && (
<div className="tag-suggestions">
{tagSuggestions.map((tag, index) => (
<button
key={index}
className="tag-suggestion"
onClick={() => handleTagClick(tag.name)}
>
<span className="tag-name">{tag.name}</span>
<span className="tag-count">{tag.count?.toLocaleString()}</span>
</button>
))}
</div>
)}
</div>
{/* Результаты */}
<div className="search-results">
{loading ? (
<div className="loading-state">
<div className="spinner" />
<p>Поиск...</p>
</div>
) : results.length === 0 && query ? (
<div className="empty-state">
<p>Ничего не найдено</p>
<span>Попробуйте другие теги</span>
</div>
) : results.length === 0 ? (
<div className="empty-state">
<SearchIcon size={48} color="var(--text-secondary)" />
<p>Введите теги для поиска</p>
<span>Используйте e621 и gelbooru</span>
</div>
) : (
<div className="results-grid">
{results.map((item, index) => (
<div
key={`${item.source}-${item.id}`}
className="result-item card"
onClick={() => openViewer(index)}
>
<img src={item.preview} alt={`Result ${index}`} />
<div className="result-overlay">
<span className="result-source">{item.source}</span>
<span className="result-rating">{item.rating}</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Просмотрщик изображений */}
{showViewer && results[currentIndex] && (
<div className="image-viewer" onClick={() => setShowViewer(false)}>
<div className="viewer-header">
<button className="viewer-btn" onClick={() => setShowViewer(false)}>
<X size={24} />
</button>
<span className="viewer-counter">
{currentIndex + 1} / {results.length}
</span>
<button className="viewer-btn" onClick={(e) => { e.stopPropagation(); handleDownload(); }}>
<Download size={24} />
</button>
</div>
<div className="viewer-content" onClick={e => e.stopPropagation()}>
<img src={results[currentIndex].url} alt="Full view" />
</div>
<div className="viewer-nav">
<button
className="nav-btn"
onClick={(e) => { e.stopPropagation(); handlePrev(); }}
disabled={currentIndex === 0}
>
<ChevronLeft size={32} />
</button>
<button
className="nav-btn"
onClick={(e) => { e.stopPropagation(); handleNext(); }}
disabled={currentIndex === results.length - 1}
>
<ChevronRight size={32} />
</button>
</div>
<div className="viewer-info">
<div className="info-tags">
{results[currentIndex].tags.slice(0, 5).map((tag, i) => (
<span key={i} className="info-tag">{tag}</span>
))}
</div>
<div className="info-stats">
<span>Score: {results[currentIndex].score}</span>
<span>Source: {results[currentIndex].source}</span>
</div>
</div>
</div>
)}
</div>
)
}