nakama/frontend/src/pages/Feed.jsx

339 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'
import { getPosts, getPost } from '../utils/api'
import PostCard from '../components/PostCard'
import CreatePostModal from '../components/CreatePostModal'
import OnboardingPost from '../components/OnboardingPost'
import AuthModal from '../components/AuthModal'
import { Plus, Settings } from 'lucide-react'
import { hapticFeedback } from '../utils/telegram'
import './Feed.css'
export default function Feed({ user }) {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showAuthModal, setShowAuthModal] = useState(false) // Модалка авторизации
const [authReason, setAuthReason] = useState('') // Причина показа модалки
const [filter, setFilter] = useState('all')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [highlightPostId, setHighlightPostId] = useState(null)
const [onboardingVisible, setOnboardingVisible] = useState({
welcome: true,
tags: true,
media: true
})
useEffect(() => {
// Проверить параметр post в URL
const postId = searchParams.get('post')
if (postId) {
setHighlightPostId(postId)
// Загрузить конкретный пост если его нет в списке
loadSpecificPost(postId)
} else {
loadPosts()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, searchParams])
const loadSpecificPost = async (postId) => {
try {
setLoading(true)
// Сначала проверить, есть ли пост уже в загруженных
const existingPost = posts.find(p => p._id === postId)
if (existingPost) {
// Если пост уже загружен, просто прокрутить к нему
setLoading(false)
setTimeout(() => {
const element = document.getElementById(`post-${postId}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, 100)
return
}
// Попытаться загрузить конкретный пост по ID
let foundPost = null
try {
foundPost = await getPost(postId)
console.log('[Feed] Загружен конкретный пост:', foundPost?._id)
} catch (postError) {
console.error('[Feed] Ошибка загрузки конкретного поста:', postError)
// Если не удалось загрузить конкретный пост, попробуем найти в ленте
}
// Загрузить ленту постов
const data = await getPosts({ filter, page: 1 })
console.log('[Feed] Загружена лента, найдено постов:', data.posts.length)
// Если конкретный пост был загружен, добавить его в начало
if (foundPost) {
// Проверить, нет ли его уже в ленте
const inFeed = data.posts.find(p => p._id === postId)
if (!inFeed) {
// Если поста нет в ленте, добавить его в начало
setPosts([foundPost, ...data.posts])
} else {
// Если пост уже в ленте, просто использовать ленту
setPosts(data.posts)
}
} else {
// Если конкретный пост не загружен, использовать только ленту
setPosts(data.posts)
}
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)
}
}, 300)
} catch (error) {
console.error('[Feed] Ошибка загрузки поста:', error)
await loadPosts()
} finally {
setLoading(false)
}
}
const loadPosts = async (pageNum = 1) => {
try {
setLoading(true)
const params = {
filter: filter, // 'all', 'interests', 'following'
page: pageNum
}
const data = await getPosts(params)
// Фильтруем посты без автора (защита от ошибок)
const validPosts = data.posts.filter(post => post.author)
if (pageNum === 1) {
setPosts(validPosts)
} else {
setPosts(prev => [...prev, ...validPosts])
}
setHasMore(pageNum < data.totalPages)
setPage(pageNum)
} catch (error) {
console.error('Ошибка загрузки постов:', error)
} finally {
setLoading(false)
}
}
const handleCreatePost = () => {
// Проверка: гость не может создавать посты
if (user?.isGuest) {
setAuthReason('Войдите, чтобы публиковать посты')
setShowAuthModal(true)
hapticFeedback('error')
return
}
hapticFeedback('light')
setShowCreateModal(true)
}
const handlePostCreated = (newPost) => {
setPosts(prev => [newPost, ...prev])
setShowCreateModal(false)
}
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))
}
const handleLoadMore = () => {
if (!loading && hasMore) {
loadPosts(page + 1)
}
}
return (
<div className="feed-page">
{/* Хедер */}
<div className="feed-header">
<h1>Nakama</h1>
<button className="create-btn" onClick={handleCreatePost}>
<Plus size={20} />
</button>
</div>
{/* Фильтры */}
<div className="feed-filters">
<button
className={`filter-btn ${filter === 'all' ? 'active' : ''}`}
onClick={() => {
setFilter('all')
setPage(1)
}}
>
Все
</button>
<button
className={`filter-btn ${filter === 'interests' ? 'active' : ''}`}
onClick={() => {
setFilter('interests')
setPage(1)
}}
>
По интересам
</button>
<button
className={`filter-btn ${filter === 'following' ? 'active' : ''}`}
onClick={() => {
setFilter('following')
setPage(1)
}}
>
Подписки
</button>
</div>
{/* Посты */}
<div className="feed-content">
{/* 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')}
/>
)}
{loading && posts.length === 0 ? (
<div className="loading-state">
<div className="spinner" />
</div>
) : posts.length === 0 ? (
<div className="empty-state">
{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>
</>
)}
</div>
) : (
<>
{posts.map(post => (
<div
key={post._id}
className={highlightPostId === post._id ? 'post-highlight' : ''}
id={`post-${post._id}`}
>
<PostCard post={post} currentUser={user} onUpdate={loadPosts} />
</div>
))}
{hasMore && (
<button
className="load-more-btn"
onClick={handleLoadMore}
disabled={loading}
>
{loading ? 'Загрузка...' : 'Загрузить ещё'}
</button>
)}
</>
)}
</div>
{/* Модальное окно создания поста */}
{showCreateModal && (
<CreatePostModal
user={user}
onClose={() => setShowCreateModal(false)}
onPostCreated={handlePostCreated}
/>
)}
{/* Модальное окно авторизации */}
{showAuthModal && (
<AuthModal
reason={authReason}
onClose={() => setShowAuthModal(false)}
onAuth={() => {
setShowAuthModal(false)
// После авторизации перезагружаем страницу
window.location.reload()
}}
/>
)}
</div>
)
}