sozsoft-platform/ui/src/views/intranet/SocialWall/PostItem.tsx

410 lines
16 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef, useEffect } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { motion, AnimatePresence } from 'framer-motion'
import classNames from 'classnames'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/tr'
import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa'
import MediaLightbox from './MediaLightbox'
import LocationMap from './LocationMap'
import UserProfileCard from './UserProfileCard'
import { SocialPostDto } from '@/proxy/intranet/models'
import { useStoreState } from '@/store/store'
import { AVATAR_URL } from '@/constants/app.constant'
import { Avatar } from '@/components/ui'
dayjs.extend(relativeTime)
dayjs.locale('tr')
interface PostItemProps {
post: SocialPostDto
onLike: (postId: string) => void
onComment: (postId: string, content: string) => void
onDelete: (postId: string) => void
onVote: (postId: string, optionId: string) => void
}
const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete, onVote }) => {
const { translate } = useLocalization()
const [showComments, setShowComments] = useState(false)
const [commentText, setCommentText] = useState('')
const [showAllImages, setShowAllImages] = useState(false)
const [lightboxOpen, setLightboxOpen] = useState(false)
const [lightboxIndex, setLightboxIndex] = useState(0)
const [showUserCard, setShowUserCard] = useState(false)
const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const { user } = useStoreState((state) => state.auth)
// Intersection Observer for video autoplay/pause
useEffect(() => {
const video = videoRef.current
if (!video) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Video ekranda görünür - oynat
video.play().catch((err) => {
console.log('Video autoplay failed:', err)
})
} else {
// Video ekrandan çıktı - durdur
video.pause()
}
})
},
{
threshold: 0.5, // Video %50 görünür olduğunda oynat
},
)
observer.observe(video)
return () => {
observer.disconnect()
}
}, [post.media?.type])
const handleSubmitComment = (e: React.FormEvent) => {
e.preventDefault()
if (commentText.trim()) {
onComment(post.id, commentText)
setCommentText('')
}
}
const getImageLayout = (images: string[]) => {
const count = images.length
if (count === 1) return 'single'
if (count === 2) return 'double'
if (count === 3) return 'triple'
return 'multiple'
}
const renderMedia = () => {
if (!post.media) return null
switch (post.media.type) {
case 'image':
if (post.media.urls && post.media.urls.length > 0) {
const layout = getImageLayout(post.media.urls)
const displayImages = showAllImages ? post.media.urls : post.media.urls.slice(0, 4)
const hasMore = post.media.urls.length > 4
return (
<>
<div
className={classNames('mt-3 rounded-lg overflow-hidden', {
'grid gap-1': layout !== 'single',
'grid-cols-2': layout === 'double' || layout === 'multiple',
'grid-cols-3': layout === 'triple',
})}
>
{displayImages.map((url, index) => (
<div
key={index}
className={classNames('relative', {
'col-span-2': layout === 'triple' && index === 0,
'aspect-video': layout === 'single',
'aspect-square': layout !== 'single',
})}
>
<img
src={url}
alt={`Post image ${index + 1}`}
className="w-full h-full object-cover cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => {
setLightboxIndex(index)
setLightboxOpen(true)
}}
/>
{hasMore && index === 3 && !showAllImages && post.media?.urls && (
<div
className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center cursor-pointer"
onClick={(e) => {
e.stopPropagation()
setShowAllImages(true)
}}
>
<span className="text-white text-2xl font-bold">
+{post.media.urls.length - 4}
</span>
</div>
)}
</div>
))}
</div>
<MediaLightbox
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
media={{ type: 'image', urls: post.media.urls }}
startIndex={lightboxIndex}
/>
</>
)
}
break
case 'video':
if (post.media.urls && post.media.urls.length > 0) {
return (
<>
<div
className="mt-3 rounded-lg overflow-hidden cursor-pointer relative group"
onClick={() => setLightboxOpen(true)}
>
<video
ref={videoRef}
src={post.media.urls[0]}
className="w-full max-h-96 object-cover"
controls
playsInline
muted
loop
/>
</div>
<MediaLightbox
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
media={{ type: 'video', urls: [post.media.urls[0]] }}
/>
</>
)
}
break
case 'poll':
if (post.media.pollQuestion && post.media.pollOptions) {
const isExpired = post.media.pollEndsAt ? new Date() > post.media.pollEndsAt : false
const hasVoted = !!post.media.pollUserVoteId
const totalVotes = post.media.pollTotalVotes || 0
const pollUserVoteId = post.media.pollUserVoteId
return (
<div className="mt-3 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">
{post.media.pollQuestion}
</h4>
<div className="space-y-2">
{post.media.pollOptions.map((option) => {
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0
const isSelected = pollUserVoteId === option.id
return (
<button
key={option.id}
onClick={() => !hasVoted && !isExpired && onVote(post.id, option.id)}
disabled={hasVoted || isExpired}
className={classNames(
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all',
{
'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500': isSelected,
'bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500':
!isSelected && !hasVoted && !isExpired,
'bg-white dark:bg-gray-600 cursor-not-allowed': hasVoted || isExpired,
},
)}
>
{hasVoted && (
<div
className="absolute inset-y-0 left-0 bg-blue-200 dark:bg-blue-800 transition-all"
style={{ width: `${percentage}%` }}
/>
)}
<div className="relative z-10 flex justify-between items-center">
<span className="font-medium text-gray-900 dark:text-gray-100">
{option.text}
</span>
{hasVoted && (
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{percentage.toFixed(0)}%
</span>
)}
</div>
</button>
)
})}
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{totalVotes} oy {' '}
{isExpired
? 'Sona erdi'
: post.media.pollEndsAt
? dayjs(post.media.pollEndsAt).fromNow() + ' bitiyor'
: ''}
</div>
</div>
)
}
break
}
return null
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-4"
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="relative"
onMouseEnter={() => setShowUserCard(true)}
onMouseLeave={() => setShowUserCard(false)}
>
<Avatar size={32} shape="circle" src={AVATAR_URL(post.user.id, post.user.tenantId)} />
<AnimatePresence>
{showUserCard && (
<UserProfileCard
user={{
id: post.user.id || '',
name: post.user.fullName || '',
title: post.user.jobPositions?.[0]?.name || '',
email: post.user.email,
phoneNumber: post.user.phoneNumber,
department: post.user.departments?.[0]?.name,
tenantId: post.user.tenantId || '',
}}
position="bottom"
/>
)}
</AnimatePresence>
</div>
<div>
<h3 className="text-sm text-gray-900 dark:text-gray-100">{post.user.fullName}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{post.user.jobPositions?.[0]?.name || ''} {dayjs(post.creationTime).fromNow()}
</p>
</div>
</div>
{post.isOwnPost && (
<button
onClick={() => onDelete(post.id)}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
title={translate('::App.Platform.Intranet.SocialWall.PostItem.DeletePost')}
>
<FaTrash className="w-5 h-5" />
</button>
)}
</div>
{/* Content */}
<div className="mb-3">
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{post.content}</p>
{renderMedia()}
</div>
{/* Actions */}
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
<button
onClick={() => onLike(post.id)}
className={classNames(
'flex items-center gap-2 transition-colors',
post.isLiked
? 'text-red-600 hover:text-red-700'
: 'text-gray-600 dark:text-gray-400 hover:text-red-600',
)}
>
{post.isLiked ? <FaHeart className="w-5 h-5" /> : <FaRegHeart className="w-5 h-5" />}
<span className="text-sm font-medium">{post.likeCount}</span>
</button>
<button
onClick={() => setShowComments(!showComments)}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 transition-colors"
>
<FaRegCommentAlt className="w-5 h-5" />
<span className="text-sm font-medium">{post.comments.length}</span>
</button>
</div>
{/* Comments Section */}
<AnimatePresence>
{showComments && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700"
>
{/* Comment Form */}
<form onSubmit={handleSubmitComment} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder={translate(
'::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder',
)}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-full bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={!commentText.trim()}
className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
<FaPaperPlane className="w-5 h-5" />
</button>
</div>
</form>
{/* Comments List */}
<div className="space-y-3">
{post.comments.map((comment) => (
<div key={comment.id} className="flex gap-3">
<div
className="relative"
onMouseEnter={() => setHoveredCommentAuthor(comment.id)}
onMouseLeave={() => setHoveredCommentAuthor(null)}
>
<Avatar
size={32}
shape="circle"
src={AVATAR_URL(comment.creator.id, comment.creator.tenantId)}
/>
<AnimatePresence>
{hoveredCommentAuthor === comment.id && (
<UserProfileCard
user={{
id: comment.creator.id || '',
name: comment.creator.fullName || '',
title: comment.creator.jobPositions?.[0]?.name || '',
tenantId: comment.creator.tenantId || '',
}}
position="bottom"
/>
)}
</AnimatePresence>
</div>
<div className="flex-1">
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{comment.creator.fullName}
</h4>
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-4">
{dayjs(comment.creationTime).fromNow()}
</p>
</div>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
export default PostItem