422 lines
16 KiB
TypeScript
422 lines
16 KiB
TypeScript
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
|