nakama/frontend/src/pages/Feed.jsx

339 lines
11 KiB
React
Raw Normal View History

2025-11-03 20:35:01 +00:00
import { useState, useEffect } from 'react'
2025-12-07 22:29:08 +00:00
import { useSearchParams, useNavigate } from 'react-router-dom'
2025-12-11 00:21:17 +00:00
import { getPosts, getPost } from '../utils/api'
2025-11-03 20:35:01 +00:00
import PostCard from '../components/PostCard'
import CreatePostModal from '../components/CreatePostModal'
2026-01-01 17:57:05 +00:00
import OnboardingPost from '../components/OnboardingPost'
import AuthModal from '../components/AuthModal'
2025-12-07 22:29:08 +00:00
import { Plus, Settings } from 'lucide-react'
2025-11-03 20:35:01 +00:00
import { hapticFeedback } from '../utils/telegram'
import './Feed.css'
export default function Feed({ user }) {
2025-11-04 21:51:05 +00:00
const [searchParams] = useSearchParams()
2025-12-07 22:29:08 +00:00
const navigate = useNavigate()
2025-11-03 20:35:01 +00:00
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
2026-01-01 17:57:05 +00:00
const [showAuthModal, setShowAuthModal] = useState(false) // Модалка авторизации
const [authReason, setAuthReason] = useState('') // Причина показа модалки
2025-11-03 20:35:01 +00:00
const [filter, setFilter] = useState('all')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
2025-11-04 21:51:05 +00:00
const [highlightPostId, setHighlightPostId] = useState(null)
2026-01-01 17:57:05 +00:00
const [onboardingVisible, setOnboardingVisible] = useState({
welcome: true,
tags: true,
media: true
})
2025-11-03 20:35:01 +00:00
useEffect(() => {
2025-11-04 21:51:05 +00:00
// Проверить параметр post в URL
const postId = searchParams.get('post')
if (postId) {
setHighlightPostId(postId)
// Загрузить конкретный пост если его нет в списке
loadSpecificPost(postId)
} else {
loadPosts()
}
2025-11-04 22:41:35 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2025-11-04 21:51:05 +00:00
}, [filter, searchParams])
const loadSpecificPost = async (postId) => {
try {
2025-12-11 00:57:03 +00:00
setLoading(true)
2025-12-11 00:21:17 +00:00
// Сначала проверить, есть ли пост уже в загруженных
const existingPost = posts.find(p => p._id === postId)
2025-11-04 21:51:05 +00:00
2025-12-11 00:21:17 +00:00
if (existingPost) {
// Если пост уже загружен, просто прокрутить к нему
2025-12-11 00:57:03 +00:00
setLoading(false)
2025-12-11 00:21:17 +00:00
setTimeout(() => {
const element = document.getElementById(`post-${postId}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, 100)
return
}
// Попытаться загрузить конкретный пост по ID
2025-12-11 00:57:03 +00:00
let foundPost = null
2025-12-11 00:21:17 +00:00
try {
2025-12-11 00:57:03 +00:00
foundPost = await getPost(postId)
console.log('[Feed] Загружен конкретный пост:', foundPost?._id)
2025-12-11 00:21:17 +00:00
} catch (postError) {
2025-12-11 00:57:03 +00:00
console.error('[Feed] Ошибка загрузки конкретного поста:', postError)
2025-12-11 00:21:17 +00:00
// Если не удалось загрузить конкретный пост, попробуем найти в ленте
}
2025-12-11 00:57:03 +00:00
// Загрузить ленту постов
2025-12-11 00:21:17 +00:00
const data = await getPosts({ filter, page: 1 })
2025-12-11 00:57:03 +00:00
console.log('[Feed] Загружена лента, найдено постов:', data.posts.length)
2025-12-11 00:21:17 +00:00
2025-12-11 00:57:03 +00:00
// Если конкретный пост был загружен, добавить его в начало
if (foundPost) {
// Проверить, нет ли его уже в ленте
const inFeed = data.posts.find(p => p._id === postId)
if (!inFeed) {
// Если поста нет в ленте, добавить его в начало
setPosts([foundPost, ...data.posts])
} else {
// Если пост уже в ленте, просто использовать ленту
setPosts(data.posts)
}
2025-11-04 21:51:05 +00:00
} else {
2025-12-11 00:57:03 +00:00
// Если конкретный пост не загружен, использовать только ленту
setPosts(data.posts)
2025-11-04 21:51:05 +00:00
}
2025-12-11 00:57:03 +00:00
setHasMore(1 < data.totalPages)
setPage(1)
// Прокрутить к посту после загрузки
setTimeout(() => {
const element = document.getElementById(`post-${postId}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
} else {
console.warn('[Feed] Элемент поста не найден после загрузки:', postId)
2025-12-15 00:37:34 +00:00
}
2025-12-11 00:57:03 +00:00
}, 300)
2025-11-04 21:51:05 +00:00
} catch (error) {
2025-12-11 00:57:03 +00:00
console.error('[Feed] Ошибка загрузки поста:', error)
2025-11-04 21:51:05 +00:00
await loadPosts()
2025-12-11 00:57:03 +00:00
} finally {
setLoading(false)
2025-11-04 21:51:05 +00:00
}
}
2025-11-03 20:35:01 +00:00
const loadPosts = async (pageNum = 1) => {
try {
setLoading(true)
2025-12-07 22:15:00 +00:00
const params = {
filter: filter, // 'all', 'interests', 'following'
page: pageNum
2025-11-03 20:35:01 +00:00
}
const data = await getPosts(params)
2025-12-01 05:40:27 +00:00
// Фильтруем посты без автора (защита от ошибок)
const validPosts = data.posts.filter(post => post.author)
2025-11-03 20:35:01 +00:00
if (pageNum === 1) {
2025-12-01 05:40:27 +00:00
setPosts(validPosts)
2025-11-03 20:35:01 +00:00
} else {
2025-12-01 05:40:27 +00:00
setPosts(prev => [...prev, ...validPosts])
2025-11-03 20:35:01 +00:00
}
setHasMore(pageNum < data.totalPages)
setPage(pageNum)
} catch (error) {
console.error('Ошибка загрузки постов:', error)
} finally {
setLoading(false)
}
}
const handleCreatePost = () => {
2026-01-01 17:57:05 +00:00
// Проверка: гость не может создавать посты
if (user?.isGuest) {
setAuthReason('Войдите, чтобы публиковать посты')
setShowAuthModal(true)
hapticFeedback('error')
return
}
2025-11-03 20:35:01 +00:00
hapticFeedback('light')
setShowCreateModal(true)
}
const handlePostCreated = (newPost) => {
setPosts(prev => [newPost, ...prev])
setShowCreateModal(false)
}
2026-01-01 17:57:05 +00:00
const handleOnboardingAction = (type) => {
hapticFeedback('light')
if (type === 'welcome' || type === 'tags') {
navigate('/profile')
} else if (type === 'media') {
navigate('/media')
}
setOnboardingVisible(prev => ({ ...prev, [type]: false }))
const dismissed = JSON.parse(localStorage.getItem('onboarding_dismissed') || '{}')
dismissed[type] = true
localStorage.setItem('onboarding_dismissed', JSON.stringify(dismissed))
}
const handleOnboardingDismiss = (type) => {
hapticFeedback('light')
setOnboardingVisible(prev => ({ ...prev, [type]: false }))
const dismissed = JSON.parse(localStorage.getItem('onboarding_dismissed') || '{}')
dismissed[type] = true
localStorage.setItem('onboarding_dismissed', JSON.stringify(dismissed))
}
2025-11-03 20:35:01 +00:00
const handleLoadMore = () => {
if (!loading && hasMore) {
loadPosts(page + 1)
}
}
return (
<div className="feed-page">
{/* Хедер */}
<div className="feed-header">
2025-11-20 21:32:48 +00:00
<h1>Nakama</h1>
2025-11-03 20:35:01 +00:00
<button className="create-btn" onClick={handleCreatePost}>
<Plus size={20} />
</button>
</div>
{/* Фильтры */}
<div className="feed-filters">
<button
className={`filter-btn ${filter === 'all' ? 'active' : ''}`}
2025-12-07 22:15:00 +00:00
onClick={() => {
setFilter('all')
setPage(1)
}}
2025-11-03 20:35:01 +00:00
>
Все
</button>
<button
2025-12-07 22:15:00 +00:00
className={`filter-btn ${filter === 'interests' ? 'active' : ''}`}
onClick={() => {
setFilter('interests')
setPage(1)
}}
2025-11-03 20:35:01 +00:00
>
2025-12-07 22:15:00 +00:00
По интересам
2025-11-03 20:35:01 +00:00
</button>
<button
2025-12-07 22:15:00 +00:00
className={`filter-btn ${filter === 'following' ? 'active' : ''}`}
onClick={() => {
setFilter('following')
setPage(1)
}}
2025-11-03 20:35:01 +00:00
>
2025-12-07 22:15:00 +00:00
Подписки
2025-11-03 20:35:01 +00:00
</button>
</div>
{/* Посты */}
<div className="feed-content">
2026-01-01 17:57:05 +00:00
{/* Onboarding посты для новых пользователей и гостей */}
{(user?.isGuest || !user?.onboarding_completed) && onboardingVisible.welcome && (
<OnboardingPost
type="welcome"
onAction={() => handleOnboardingAction('welcome')}
onDismiss={() => handleOnboardingDismiss('welcome')}
/>
)}
{(user?.isGuest || !user?.onboarding_completed) && onboardingVisible.tags && posts.length > 2 && (
<OnboardingPost
type="tags"
onAction={() => handleOnboardingAction('tags')}
onDismiss={() => handleOnboardingDismiss('tags')}
/>
)}
2025-11-03 20:35:01 +00:00
{loading && posts.length === 0 ? (
<div className="loading-state">
<div className="spinner" />
</div>
) : posts.length === 0 ? (
<div className="empty-state">
2025-12-07 22:29:08 +00:00
{filter === 'interests' ? (
<>
<p>Нет постов по вашим интересам</p>
<p className="empty-state-hint">
Проверьте выбранные теги в настройках профиля
</p>
<button
className="btn-primary"
onClick={() => {
hapticFeedback('light')
navigate('/profile')
}}
>
<Settings size={18} style={{ marginRight: '8px' }} />
Открыть настройки
</button>
</>
) : filter === 'following' ? (
<>
<p>Нет постов от подписок</p>
<p className="empty-state-hint">
Подпишитесь на пользователей, чтобы видеть их посты здесь
</p>
<button className="btn-primary" onClick={handleCreatePost}>
Создать пост
</button>
</>
) : (
<>
<p>Пока нет постов</p>
<button className="btn-primary" onClick={handleCreatePost}>
Создать первый пост
</button>
</>
)}
2025-11-03 20:35:01 +00:00
</div>
) : (
<>
{posts.map(post => (
2025-11-04 21:51:05 +00:00
<div
key={post._id}
className={highlightPostId === post._id ? 'post-highlight' : ''}
id={`post-${post._id}`}
>
<PostCard post={post} currentUser={user} onUpdate={loadPosts} />
</div>
2025-11-03 20:35:01 +00:00
))}
{hasMore && (
<button
className="load-more-btn"
onClick={handleLoadMore}
disabled={loading}
>
{loading ? 'Загрузка...' : 'Загрузить ещё'}
</button>
)}
</>
)}
</div>
{/* Модальное окно создания поста */}
{showCreateModal && (
<CreatePostModal
user={user}
onClose={() => setShowCreateModal(false)}
onPostCreated={handlePostCreated}
/>
)}
2026-01-01 17:57:05 +00:00
{/* Модальное окно авторизации */}
{showAuthModal && (
<AuthModal
reason={authReason}
onClose={() => setShowAuthModal(false)}
onAuth={() => {
setShowAuthModal(false)
// После авторизации перезагружаем страницу
window.location.reload()
}}
/>
)}
2025-11-03 20:35:01 +00:00
</div>
)
}