erp-platform/ui/src/views/intranet/SocialWall/PostItem.tsx
2025-11-03 23:31:28 +03:00

422 lines
16 KiB
TypeScript
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 React, { useState, useRef, useEffect } from 'react'
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'
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 [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)
// 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)}
>
<img
src={post.employee.avatar || 'https://i.pravatar.cc/150?img=1'}
alt={post.employee.fullName}
className="w-12 h-12 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
/>
<AnimatePresence>
{showUserCard && (
<UserProfileCard
user={{
id: post.employee.id,
name: post.employee.fullName,
avatar: post.employee.avatar || 'https://i.pravatar.cc/150?img=1',
title: post.employee.jobPosition?.name || 'Çalışan',
email: post.employee.email,
phoneNumber: post.employee.phoneNumber,
department: post.employee.department?.name,
location: post.employee.workLocation
}}
position="bottom"
/>
)}
</AnimatePresence>
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{post.employee.fullName}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{post.employee.jobPosition?.name || 'Çalışan'} {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="Gönderiyi sil"
>
<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()}
{/* Location */}
{post.locationJson && (
<div className="mt-3">
<LocationMap location={post.locationJson} />
</div>
)}
</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="Yorum yazın..."
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)}
>
<img
src={comment.creator.avatar || 'https://i.pravatar.cc/150?img=1'}
alt={comment.creator.fullName}
className="w-8 h-8 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
/>
<AnimatePresence>
{hoveredCommentAuthor === comment.id && (
<UserProfileCard
user={{
id: comment.creator.id,
name: comment.creator.fullName,
avatar: comment.creator.avatar || 'https://i.pravatar.cc/150?img=1',
title: comment.creator.jobPosition?.name || 'Çalışan'
}}
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