nakama/frontend/src/components/PostCard.jsx

340 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 } from 'react'
import { useNavigate } from 'react-router-dom'
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react'
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram'
import './PostCard.css'
const TAG_COLORS = {
furry: '#FF8A33',
anime: '#4A90E2',
other: '#A0A0A0'
}
const TAG_NAMES = {
furry: 'Furry',
anime: 'Anime',
other: 'Other'
}
export default function PostCard({ post, currentUser, onUpdate }) {
const navigate = useNavigate()
const [liked, setLiked] = useState(post.likes?.includes(currentUser.id) || false)
const [likesCount, setLikesCount] = useState(post.likes?.length || 0)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [showFullView, setShowFullView] = useState(false)
// Проверка на существование автора
if (!post.author) {
console.warn('[PostCard] Post without author:', post._id)
return null // Не показываем посты без автора
}
// Поддержка и старого поля imageUrl и нового images
// Фильтруем старые URL из Telegram API
const allImages = post.images && post.images.length > 0 ? post.images : (post.imageUrl ? [post.imageUrl] : [])
const images = allImages.filter(img => {
// Игнорируем старые URL из Telegram API
if (img && typeof img === 'string' && img.includes('api.telegram.org/file/bot')) {
console.warn('[PostCard] Skipping old Telegram URL:', img)
return false
}
return true
})
const handleLike = async () => {
try {
hapticFeedback('light')
const result = await likePost(post._id)
setLiked(result.liked)
setLikesCount(result.likes)
if (result.liked) {
hapticFeedback('success')
}
} catch (error) {
console.error('Ошибка лайка:', error)
}
}
const handleDelete = async () => {
const confirmed = await showConfirm('Удалить этот пост?')
if (confirmed) {
try {
await deletePost(post._id)
hapticFeedback('success')
onUpdate()
} catch (error) {
console.error('Ошибка удаления:', error)
}
}
}
const formatDate = (date) => {
const d = new Date(date)
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
}
const goToProfile = () => {
if (post.author?._id) {
navigate(`/user/${post.author._id}`)
}
}
const openFullView = () => {
if (images.length > 0) {
setShowFullView(true)
hapticFeedback('light')
}
}
const handleNext = () => {
if (currentImageIndex < images.length - 1) {
setCurrentImageIndex(currentImageIndex + 1)
hapticFeedback('light')
}
}
const handlePrev = () => {
if (currentImageIndex > 0) {
setCurrentImageIndex(currentImageIndex - 1)
hapticFeedback('light')
}
}
const handleDownloadImage = async () => {
try {
hapticFeedback('light')
const imageUrl = images[currentImageIndex]
await sendPhotoToTelegram(imageUrl)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка отправки фото:', error)
hapticFeedback('error')
}
}
const handleRepost = () => {
try {
hapticFeedback('light')
// Получить имя бота из переменных окружения или использовать дефолтное
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
// Создать deeplink для открытия поста в миниапп
// Используем startapp для миниаппов - это правильный формат для передачи параметра в миниапп
const deeplink = `https://t.me/${botName}?startapp=post_${post._id}`
// Открыть нативное окно "Поделиться" в Telegram
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(deeplink)}&text=${encodeURIComponent('Смотри пост в Nakama!')}`
openTelegramLink(shareUrl)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка репоста:', error)
hapticFeedback('error')
}
}
return (
<div className="post-card card fade-in">
{/* Хедер поста */}
<div className="post-header">
<div className="post-author" onClick={goToProfile}>
<img
src={post.author?.photoUrl || '/default-avatar.png'}
alt={post.author?.username || post.author?.firstName || 'User'}
className="author-avatar"
onError={(e) => {
e.target.src = '/default-avatar.png'
}}
/>
<div className="author-info">
<div className="author-name">
{post.author?.firstName || ''} {post.author?.lastName || ''}
{!post.author?.firstName && !post.author?.lastName && 'Пользователь'}
</div>
<div className="post-date">
@{post.author?.username || post.author?.firstName || 'user'} · {formatDate(post.createdAt)}
</div>
</div>
</div>
<button
className="menu-btn"
onClick={(e) => {
e.stopPropagation()
navigate(`/post/${post._id}/menu`)
}}
>
<MoreVertical size={20} />
</button>
</div>
{/* Контент */}
{post.content && (
<div className="post-content">
{post.content}
</div>
)}
{/* Изображения */}
{images.length > 0 && (
<div className="post-images">
<div className="image-carousel" onClick={openFullView} style={{ cursor: 'pointer' }}>
<img src={images[currentImageIndex]} alt={`Image ${currentImageIndex + 1}`} />
{images.length > 1 && (
<div className="carousel-dots">
{images.map((_, index) => (
<span
key={index}
className={`dot ${index === currentImageIndex ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setCurrentImageIndex(index); }}
/>
))}
</div>
)}
{/* Индикатор что можно открыть fullview */}
<div className="fullview-hint">
<ZoomIn size={20} />
</div>
</div>
</div>
)}
{/* Теги */}
<div className="post-tags">
{post.tags.map((tag, index) => (
<span
key={index}
className="post-tag"
style={{ backgroundColor: TAG_COLORS[tag] }}
>
{TAG_NAMES[tag]}
</span>
))}
{post.isNSFW && (
<span className="nsfw-badge">NSFW</span>
)}
</div>
{/* Действия */}
<div className="post-actions">
<button
className={`action-btn ${liked ? 'active' : ''}`}
onClick={(e) => {
e.stopPropagation()
handleLike()
}}
>
<Heart size={20} fill={liked ? '#FF3B30' : 'none'} stroke={liked ? '#FF3B30' : 'currentColor'} />
<span>{likesCount}</span>
</button>
<button
className="action-btn"
onClick={(e) => {
e.stopPropagation()
navigate(`/post/${post._id}/comments`)
}}
>
<MessageCircle size={20} stroke="currentColor" />
<span>{post.comments.length}</span>
</button>
<button
className="action-btn"
onClick={(e) => {
e.stopPropagation()
handleRepost()
}}
title="Поделиться постом"
>
<Share2 size={20} stroke="currentColor" />
</button>
{images.length > 0 && (
<button
className="action-btn"
onClick={async (e) => {
e.stopPropagation()
try {
hapticFeedback('light')
const imageUrl = images[currentImageIndex] || post.imageUrl
await sendPhotoToTelegram(imageUrl)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка отправки фото:', error)
hapticFeedback('error')
}
}}
title="Отправить фото в Telegram"
>
<Send size={20} stroke="currentColor" />
</button>
)}
</div>
{/* Fullview модал */}
{showFullView && (
<div className="image-fullview" onClick={() => setShowFullView(false)}>
<div className="fullview-header">
<button className="fullview-btn" onClick={(e) => { e.stopPropagation(); setShowFullView(false); }}>
<X size={24} />
</button>
<span className="fullview-counter">
{currentImageIndex + 1} / {images.length}
</span>
<button
className="fullview-btn"
onClick={(e) => { e.stopPropagation(); handleDownloadImage(); }}
title="Отправить в Telegram"
>
<Download size={24} />
</button>
</div>
<div className="fullview-content" onClick={(e) => e.stopPropagation()}>
<img
src={images[currentImageIndex]}
alt={`Full view ${currentImageIndex + 1}`}
draggable={false}
/>
{images.length > 1 && (
<>
{currentImageIndex > 0 && (
<button className="fullview-nav-btn prev" onClick={(e) => { e.stopPropagation(); handlePrev(); }}>
<ChevronLeft size={32} />
</button>
)}
{currentImageIndex < images.length - 1 && (
<button className="fullview-nav-btn next" onClick={(e) => { e.stopPropagation(); handleNext(); }}>
<ChevronRight size={32} />
</button>
)}
</>
)}
</div>
{images.length > 1 && (
<div className="fullview-dots">
{images.map((_, index) => (
<span
key={index}
className={`fullview-dot ${index === currentImageIndex ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setCurrentImageIndex(index); }}
/>
))}
</div>
)}
</div>
)}
</div>
)
}