nakama/frontend/src/components/PostCard.jsx

300 lines
9.3 KiB
JavaScript

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn } from 'lucide-react'
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
import { hapticFeedback, showConfirm } 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))
const [likesCount, setLikesCount] = useState(post.likes.length)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [showFullView, setShowFullView] = useState(false)
// Поддержка и старого поля imageUrl и нового images
const images = post.images && post.images.length > 0 ? post.images : (post.imageUrl ? [post.imageUrl] : [])
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 = () => {
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')
}
}
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}
className="author-avatar"
/>
<div className="author-info">
<div className="author-name">
{post.author.firstName} {post.author.lastName}
</div>
<div className="post-date">
@{post.author.username} · {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 && (
<>
{currentImageIndex > 0 && (
<button className="carousel-btn prev" onClick={(e) => { e.stopPropagation(); setCurrentImageIndex(currentImageIndex - 1); }}>
<ChevronLeft size={24} />
</button>
)}
{currentImageIndex < images.length - 1 && (
<button className="carousel-btn next" onClick={(e) => { e.stopPropagation(); setCurrentImageIndex(currentImageIndex + 1); }}>
<ChevronRight size={24} />
</button>
)}
<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>
{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>
)
}