From ed8917e8dd266734a7bac19d69c0b06af22cd12f Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Thu, 4 Dec 2025 23:47:07 +0300 Subject: [PATCH] Update files --- backend/routes/search.js | 2 +- frontend/src/components/CommentsModal.css | 29 +++++ frontend/src/components/CommentsModal.jsx | 114 +++++++++++++++----- frontend/src/components/FollowListModal.css | 37 ++++--- frontend/src/components/FollowListModal.jsx | 37 +++---- frontend/src/pages/Search.css | 45 ++++++++ frontend/src/pages/Search.jsx | 78 +++++++++++--- 7 files changed, 268 insertions(+), 74 deletions(-) diff --git a/backend/routes/search.js b/backend/routes/search.js index 9622fa7..c1414e4 100644 --- a/backend/routes/search.js +++ b/backend/routes/search.js @@ -57,7 +57,7 @@ function createProxyUrl(originalUrl) { // Эндпоинт для проксирования изображений // Используем более мягкий rate limiter для прокси -router.get('/proxy/:encodedUrl', proxyLimiter, async (req, res) => { +router.get('/proxy/:encodedUrl', async (req, res) => { try { const { encodedUrl } = req.params; diff --git a/frontend/src/components/CommentsModal.css b/frontend/src/components/CommentsModal.css index 487ce51..d6dc958 100644 --- a/frontend/src/components/CommentsModal.css +++ b/frontend/src/components/CommentsModal.css @@ -115,6 +115,35 @@ gap: 16px; } +.loading-state { + padding: 40px 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: var(--text-secondary); +} + +.loading-state .spinner { + width: 32px; + height: 32px; + border: 3px solid var(--divider-color); + border-top-color: var(--button-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-state p { + font-size: 14px; + margin: 0; +} + .empty-comments { padding: 60px 20px; text-align: center; diff --git a/frontend/src/components/CommentsModal.jsx b/frontend/src/components/CommentsModal.jsx index 8740051..800bce0 100644 --- a/frontend/src/components/CommentsModal.jsx +++ b/frontend/src/components/CommentsModal.jsx @@ -1,7 +1,7 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { X, Send } from 'lucide-react' -import { commentPost } from '../utils/api' +import { commentPost, getPosts } from '../utils/api' import { hapticFeedback } from '../utils/telegram' import { decodeHtmlEntities } from '../utils/htmlEntities' import './CommentsModal.css' @@ -9,7 +9,50 @@ import './CommentsModal.css' export default function CommentsModal({ post, onClose, onUpdate }) { const [comment, setComment] = useState('') const [loading, setLoading] = useState(false) - const [comments, setComments] = useState(post.comments || []) + const [comments, setComments] = useState([]) + const [fullPost, setFullPost] = useState(post) + const [loadingPost, setLoadingPost] = useState(false) + + // Загрузить полные данные поста с комментариями + useEffect(() => { + if (post?._id) { + loadFullPost() + } else { + setFullPost(post) + setComments(post?.comments || []) + } + }, [post?._id]) + + const loadFullPost = async () => { + try { + setLoadingPost(true) + const response = await getPosts() + const foundPost = response.posts?.find(p => p._id === post._id) + if (foundPost) { + setFullPost(foundPost) + setComments(foundPost.comments || []) + } else { + // Fallback на переданный post + setFullPost(post) + setComments(post?.comments || []) + } + } catch (error) { + console.error('[CommentsModal] Ошибка загрузки поста:', error) + // Fallback на переданный post + setFullPost(post) + setComments(post?.comments || []) + } finally { + setLoadingPost(false) + } + } + + // Проверка на существование поста + if (!post) { + console.error('[CommentsModal] Post is missing') + return null + } + + const displayPost = fullPost || post const handleSubmit = async () => { if (!comment.trim()) return @@ -19,9 +62,11 @@ export default function CommentsModal({ post, onClose, onUpdate }) { hapticFeedback('light') const result = await commentPost(post._id, comment) - setComments(result.comments) + setComments(result.comments || []) setComment('') hapticFeedback('success') + // Обновить полный пост + await loadFullPost() onUpdate() } catch (error) { console.error('Ошибка добавления комментария:', error) @@ -50,6 +95,10 @@ export default function CommentsModal({ post, onClose, onUpdate }) { } } + if (!post) { + return null + } + return createPortal(
{/* Пост */} -
-
- {post.author?.username { e.target.src = '/default-avatar.png' }} - /> -
-
- {post.author?.firstName || ''} {post.author?.lastName || ''} - {!post.author?.firstName && !post.author?.lastName && 'Пользователь'} -
-
@{post.author?.username || post.author?.firstName || 'user'}
+ {loadingPost ? ( +
+
+
+

Загрузка...

- - {post.content && ( -
{decodeHtmlEntities(post.content)}
- )} - - {((post.images && post.images.length > 0) || post.imageUrl) && ( -
- Post + ) : ( +
+
+ {displayPost.author?.username { e.target.src = '/default-avatar.png' }} + /> +
+
+ {displayPost.author?.firstName || ''} {displayPost.author?.lastName || ''} + {!displayPost.author?.firstName && !displayPost.author?.lastName && 'Пользователь'} +
+
@{displayPost.author?.username || displayPost.author?.firstName || 'user'}
+
- )} -
+ + {displayPost.content && ( +
{decodeHtmlEntities(displayPost.content)}
+ )} + + {((displayPost.images && displayPost.images.length > 0) || displayPost.imageUrl) && ( +
+ Post +
+ )} +
+ )} {/* Список комментариев */}
diff --git a/frontend/src/components/FollowListModal.css b/frontend/src/components/FollowListModal.css index f69a3fb..0976936 100644 --- a/frontend/src/components/FollowListModal.css +++ b/frontend/src/components/FollowListModal.css @@ -49,13 +49,13 @@ display: flex; justify-content: space-between; align-items: center; - padding: 16px; + padding: 12px 16px; border-bottom: 1px solid var(--divider-color); flex-shrink: 0; } .follow-list-header h2 { - font-size: 18px; + font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0; @@ -102,13 +102,21 @@ padding: 8px 0; } +.user-item-wrapper { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 16px; +} + .user-item { display: flex; align-items: center; - gap: 12px; - padding: 12px 16px; + gap: 8px; cursor: pointer; transition: background 0.2s; + padding: 4px; + border-radius: 8px; } .user-item:active { @@ -116,8 +124,8 @@ } .user-avatar { - width: 48px; - height: 48px; + width: 36px; + height: 36px; border-radius: 50%; object-fit: cover; flex-shrink: 0; @@ -129,7 +137,7 @@ } .user-name { - font-size: 15px; + font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 2px; @@ -139,7 +147,7 @@ } .user-username { - font-size: 13px; + font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; @@ -150,21 +158,22 @@ .follow-btn { display: flex; align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 20px; + justify-content: center; + gap: 8px; + width: 100%; + padding: 10px 18px; + border-radius: 12px; background: var(--button-accent); color: white; border: none; font-size: 14px; font-weight: 600; cursor: pointer; - transition: opacity 0.2s; - flex-shrink: 0; + transition: opacity 0.2s ease; } .follow-btn:active { - opacity: 0.8; + opacity: 0.85; } .follow-btn.following { diff --git a/frontend/src/components/FollowListModal.jsx b/frontend/src/components/FollowListModal.jsx index ca7bd88..664cba6 100644 --- a/frontend/src/components/FollowListModal.jsx +++ b/frontend/src/components/FollowListModal.jsx @@ -87,23 +87,24 @@ export default function FollowListModal({ users, title, onClose, currentUser }) const isFollowing = userStates[user._id]?.isFollowing || false return ( -
handleUserClick(user._id)} - > - {user.username { e.target.src = '/default-avatar.png' }} - /> -
-
- {user.firstName || ''} {user.lastName || ''} - {!user.firstName && !user.lastName && 'Пользователь'} +
+
handleUserClick(user._id)} + > + {user.username { e.target.src = '/default-avatar.png' }} + /> +
+
+ {user.firstName || ''} {user.lastName || ''} + {!user.firstName && !user.lastName && 'Пользователь'} +
+
@{user.username || user.firstName || 'user'}
-
@{user.username || user.firstName || 'user'}
{!isOwnProfile && ( @@ -113,12 +114,12 @@ export default function FollowListModal({ users, title, onClose, currentUser }) > {isFollowing ? ( <> - + Отписаться ) : ( <> - + Подписаться )} diff --git a/frontend/src/pages/Search.css b/frontend/src/pages/Search.css index 7f11d08..1939633 100644 --- a/frontend/src/pages/Search.css +++ b/frontend/src/pages/Search.css @@ -475,3 +475,48 @@ color: rgba(255, 255, 255, 0.7); } +.load-more-container { + padding: 20px; + display: flex; + justify-content: center; +} + +.load-more-btn { + padding: 12px 24px; + border-radius: 24px; + background: var(--button-accent); + color: white; + border: none; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +.load-more-btn:active { + opacity: 0.8; +} + +.loading-more { + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: var(--text-secondary); +} + +.loading-more .spinner { + width: 24px; + height: 24px; + border: 3px solid var(--divider-color); + border-top-color: var(--button-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.loading-more p { + font-size: 14px; + margin: 0; +} + diff --git a/frontend/src/pages/Search.jsx b/frontend/src/pages/Search.jsx index 613b5d0..08a3b97 100644 --- a/frontend/src/pages/Search.jsx +++ b/frontend/src/pages/Search.jsx @@ -13,7 +13,11 @@ export default function Search({ user }) { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [loading, setLoading] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) const [tagSuggestions, setTagSuggestions] = useState([]) + const [showTagSuggestions, setShowTagSuggestions] = useState(true) + const [currentPage, setCurrentPage] = useState(1) + const [hasMore, setHasMore] = useState(false) const [currentIndex, setCurrentIndex] = useState(0) const [showViewer, setShowViewer] = useState(false) const [selectedImages, setSelectedImages] = useState([]) @@ -30,12 +34,12 @@ const isVideoUrl = (url = '') => { } useEffect(() => { - if (query.length > 1) { + if (query.length > 1 && showTagSuggestions) { loadTagSuggestions() } else { setTagSuggestions([]) } - }, [query, mode]) + }, [query, mode, showTagSuggestions]) const loadTagSuggestions = async () => { try { @@ -90,21 +94,29 @@ const isVideoUrl = (url = '') => { } } - const handleSearch = async (searchQuery = query) => { + const handleSearch = async (searchQuery = query, page = 1, append = false) => { if (!searchQuery.trim()) return try { - setLoading(true) + if (page === 1) { + setLoading(true) + setResults([]) + } else { + setLoadingMore(true) + } + hapticFeedback('light') - setResults([]) + setShowTagSuggestions(false) let allResults = [] + let hasMoreResults = false if (mode === 'furry') { try { - const furryResults = await searchFurry(searchQuery, { limit: 320, page: 1 }) + const furryResults = await searchFurry(searchQuery, { limit: 320, page }) if (Array.isArray(furryResults)) { allResults = [...allResults, ...furryResults] + hasMoreResults = furryResults.length === 320 } } catch (error) { console.error('Ошибка e621 поиска:', error) @@ -113,29 +125,47 @@ const isVideoUrl = (url = '') => { if (mode === 'anime') { try { - const animeResults = await searchAnime(searchQuery, { limit: 320, page: 1 }) + const animeResults = await searchAnime(searchQuery, { limit: 320, page }) if (Array.isArray(animeResults)) { allResults = [...allResults, ...animeResults] + hasMoreResults = animeResults.length === 320 } } catch (error) { console.error('Ошибка Gelbooru поиска:', error) } } - setResults(allResults) + if (append) { + setResults(prev => [...prev, ...allResults]) + } else { + setResults(allResults) + setCurrentPage(1) + } + + setHasMore(hasMoreResults) + setCurrentPage(page) setTagSuggestions([]) if (allResults.length > 0) { hapticFeedback('success') - } else { + } else if (page === 1) { hapticFeedback('error') } } catch (error) { console.error('Ошибка поиска:', error) hapticFeedback('error') - setResults([]) + if (page === 1) { + setResults([]) + } } finally { setLoading(false) + setLoadingMore(false) + } + } + + const loadMore = () => { + if (!loadingMore && hasMore && query.trim()) { + handleSearch(query, currentPage + 1, true) } } @@ -149,6 +179,7 @@ const isVideoUrl = (url = '') => { : tagName setQuery(newQuery) + setShowTagSuggestions(false) handleSearch(newQuery) } @@ -370,8 +401,15 @@ const isVideoUrl = (url = '') => { type="text" placeholder="Поиск по тегам..." value={query} - onChange={e => setQuery(e.target.value)} - onKeyPress={e => e.key === 'Enter' && handleSearch()} + onChange={e => { + setQuery(e.target.value) + setShowTagSuggestions(true) + }} + onKeyPress={e => { + if (e.key === 'Enter') { + handleSearch() + } + }} /> {query && (
{/* Подсказки тегов */} - {tagSuggestions.length > 0 && ( + {tagSuggestions.length > 0 && showTagSuggestions && (
{tagSuggestions.map((tag, index) => (
{/* Кнопка загрузки дополнительных результатов */} + {hasMore && !loadingMore && ( +
+ +
+ )} + + {loadingMore && ( +
+
+

Загрузка...

+
+ )} {/* Кнопка отправки выбранных */} {selectionMode && selectedImages.length > 0 && (