sozsoft-platform/ui/src/views/intranet/widgets/EventModal.tsx

399 lines
15 KiB
TypeScript
Raw Normal View History

2026-05-07 14:25:06 +00:00
import React, { useState, useEffect, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
FaTimes,
FaChevronLeft,
FaChevronRight,
FaMapMarkerAlt,
FaCalendarAlt,
FaUsers,
FaExpand,
FaCommentAlt,
FaPaperPlane,
} from 'react-icons/fa'
import { EventCommentDto, EventDto } from '@/proxy/intranet/models'
import useLocale from '@/utils/hooks/useLocale'
import { currentLocalDate } from '@/utils/dateUtils'
import Avatar from '@/components/ui/Avatar/Avatar'
import { AVATAR_URL } from '@/constants/app.constant'
import { intranetService } from '@/services/intranet.service'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
interface EventModalProps {
event: EventDto
onClose: () => void
}
const imgSrc = (img: string) => {
if (
img.startsWith('data:') ||
img.startsWith('http://') ||
img.startsWith('https://') ||
img.startsWith('/')
)
return img
return `data:image/jpeg;base64,${img}`
}
const EventModal: React.FC<EventModalProps> = ({ event, onClose }) => {
const currentLocale = useLocale()
const photos = (event.photos || '').split('|').filter(Boolean)
// Photo slider state
const [activePhoto, setActivePhoto] = useState(0)
const [lightboxOpen, setLightboxOpen] = useState(false)
const [lightboxIndex, setLightboxIndex] = useState(0)
// Comments state
const [comments, setComments] = useState<EventCommentDto[]>(event.comments || [])
const [commentText, setCommentText] = useState('')
const [submitting, setSubmitting] = useState(false)
const commentInputRef = useRef<HTMLTextAreaElement>(null)
const commentsEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// Refresh comments from API on open
intranetService.getEventComments(event.id).then((res) => {
if (res.data) setComments(res.data)
})
}, [event.id])
const scrollToBottom = () => {
commentsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
const handleSubmitComment = async () => {
if (!commentText.trim() || submitting) return
setSubmitting(true)
try {
const res = await intranetService.createEventComment(event.id, commentText.trim())
if (res.data) {
setComments((prev) => [...prev, res.data!])
setCommentText('')
setTimeout(scrollToBottom, 100)
}
} finally {
setSubmitting(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmitComment()
}
}
const openLightbox = (idx: number) => {
setLightboxIndex(idx)
setLightboxOpen(true)
}
const closeLightbox = () => setLightboxOpen(false)
const prevLightbox = (e: React.MouseEvent) => {
e.stopPropagation()
setLightboxIndex((i) => (i - 1 + photos.length) % photos.length)
}
const nextLightbox = (e: React.MouseEvent) => {
e.stopPropagation()
setLightboxIndex((i) => (i + 1) % photos.length)
}
return (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 z-40"
onClick={onClose}
/>
{/* Modal */}
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-3xl flex flex-col"
style={{ maxHeight: '90vh' }}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-gray-900 dark:text-white leading-tight">
{event.name}
</h2>
<div className="flex flex-wrap items-center gap-4 mt-2 text-sm text-gray-600 dark:text-gray-400">
<span className="flex items-center gap-1">
<FaCalendarAlt className="w-3.5 h-3.5 text-green-500" />
{currentLocalDate(event.date, currentLocale || 'tr')}
</span>
{event.place && (
<span className="flex items-center gap-1">
<FaMapMarkerAlt className="w-3.5 h-3.5 text-red-500" />
{event.place}
</span>
)}
{event.participantsCount > 0 && (
<span className="flex items-center gap-1">
<FaUsers className="w-3.5 h-3.5 text-blue-500" />
{event.participantsCount}
</span>
)}
</div>
{event.user && (
<div className="flex items-center gap-2 mt-3">
<Avatar
size={28}
shape="circle"
src={AVATAR_URL(event.user.id, event.user.tenantId)}
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{event.user.fullName}
</span>
</div>
)}
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
>
<FaTimes className="w-5 h-5 text-gray-500" />
</button>
</div>
</div>
{/* Scrollable body */}
<div className="flex-1 overflow-y-auto">
{/* Photo Gallery */}
{photos.length > 0 && (
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
{/* Main photo */}
<div className="relative rounded-lg overflow-hidden bg-gray-900 mb-2">
<img
src={imgSrc(photos[activePhoto])}
alt={`${event.name} ${activePhoto + 1}`}
className="w-full object-cover cursor-pointer"
style={{ maxHeight: '320px' }}
onClick={() => openLightbox(activePhoto)}
/>
<button
onClick={() => openLightbox(activePhoto)}
className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-lg text-white transition-colors"
title="Tam ekran"
>
<FaExpand className="w-4 h-4" />
</button>
{photos.length > 1 && (
<>
<button
onClick={(e) => {
e.stopPropagation()
setActivePhoto((i) => (i - 1 + photos.length) % photos.length)
}}
className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
>
<FaChevronLeft className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation()
setActivePhoto((i) => (i + 1) % photos.length)
}}
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
>
<FaChevronRight className="w-4 h-4" />
</button>
</>
)}
</div>
{/* Thumbnail strip */}
{photos.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-1">
{photos.map((photo, idx) => (
<button
key={idx}
onClick={() => setActivePhoto(idx)}
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all ${
idx === activePhoto
? 'border-green-500 opacity-100'
: 'border-transparent opacity-60 hover:opacity-90'
}`}
>
<img
src={imgSrc(photo)}
alt={`Thumbnail ${idx + 1}`}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
)}
</div>
)}
{/* Description */}
{event.description && (
<div className="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line">
{event.description}
</p>
</div>
)}
{/* Comments Section */}
<div className="p-5">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
<FaCommentAlt className="w-4 h-4 text-green-500" />
Yorumlar ({comments.length})
</h3>
{/* Comment List */}
<div className="space-y-4 mb-4 max-h-64 overflow-y-auto">
{comments.length === 0 ? (
<p className="text-sm text-gray-400 dark:text-gray-500 text-center py-6">
Henüz yorum yok. İlk yorumu sen yap!
</p>
) : (
comments.map((comment) => (
<div key={comment.id} className="flex gap-3">
{comment.user && (
<Avatar
size={32}
shape="circle"
src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
className="flex-shrink-0"
/>
)}
<div className="flex-1 min-w-0">
<div className="bg-gray-50 dark:bg-gray-700/60 rounded-xl px-3 py-2">
<p className="text-xs font-semibold text-gray-800 dark:text-gray-200">
{comment.user?.fullName ?? 'Kullanıcı'}
</p>
<p className="text-sm text-gray-700 dark:text-gray-300 mt-0.5 whitespace-pre-line">
{comment.content}
</p>
</div>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 px-1">
{dayjs(comment.creationTime).fromNow()}
</p>
</div>
</div>
))
)}
<div ref={commentsEndRef} />
</div>
{/* Comment Input */}
<div className="flex gap-2 items-end">
<textarea
ref={commentInputRef}
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Bir yorum yaz... (Enter ile gönder)"
rows={2}
className="flex-1 resize-none px-3 py-2 text-sm rounded-xl border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 transition-colors"
/>
<button
onClick={handleSubmitComment}
disabled={!commentText.trim() || submitting}
className="p-2.5 bg-green-500 hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-colors flex-shrink-0"
>
<FaPaperPlane className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
<button
onClick={onClose}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium"
>
Kapat
</button>
</div>
</motion.div>
</div>
{/* Lightbox */}
<AnimatePresence>
{lightboxOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/95 z-[60]"
onClick={closeLightbox}
/>
<div className="fixed inset-0 z-[70] flex items-center justify-center">
<button
onClick={closeLightbox}
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
>
<FaTimes className="w-8 h-8" />
</button>
{photos.length > 1 && (
<>
<button
onClick={prevLightbox}
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
>
<FaChevronLeft className="w-6 h-6" />
</button>
<button
onClick={nextLightbox}
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
>
<FaChevronRight className="w-6 h-6" />
</button>
</>
)}
<motion.img
key={lightboxIndex}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
src={imgSrc(photos[lightboxIndex])}
alt={`${event.name} ${lightboxIndex + 1}`}
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
onClick={(e) => e.stopPropagation()}
/>
{photos.length > 1 && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
{photos.map((_, idx) => (
<button
key={idx}
onClick={(e) => {
e.stopPropagation()
setLightboxIndex(idx)
}}
className={`w-2.5 h-2.5 rounded-full transition-all ${
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
}`}
/>
))}
</div>
)}
</div>
</>
)}
</AnimatePresence>
</>
)
}
export default EventModal