sozsoft-platform/ui/src/views/intranet/widgets/EventModal.tsx
2026-06-26 22:02:53 +03:00

358 lines
13 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, useEffect, useRef } from 'react'
import {
FaChevronLeft,
FaChevronRight,
FaMapMarkerAlt,
FaCalendarAlt,
FaUsers,
FaExpand,
FaCommentAlt,
FaPaperPlane,
} from 'react-icons/fa'
import { Button, Dialog } from '@/components/ui'
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
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'
import { useLocalization } from '@/utils/hooks/useLocalization'
import ImageLightbox from './ImageLightbox'
import LikeButton from './LikeButton'
import { imageSrc } from './widgetUtils'
dayjs.extend(relativeTime)
interface EventModalProps {
event: EventDto
onClose: () => void
onLikeChange?: (event: EventDto) => void
}
function EventModalContent({ event, onClose, onLikeChange }: EventModalProps) {
const currentLocale = useLocale()
const photos = (event.photos || '').split('|').filter(Boolean)
const { translate } = useLocalization()
const { isMaximized } = useDialogContext()
// 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)
// Likes state
const [likes, setLikes] = useState(event.likes)
const [isLiked, setIsLiked] = useState(event.isLiked)
const [liking, setLiking] = useState(false)
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 handleLike = async () => {
if (liking) return
setLiking(true)
// Optimistic update
setIsLiked((prev) => !prev)
setLikes((prev) => (isLiked ? Math.max(0, prev - 1) : prev + 1))
try {
const res = await intranetService.likeEvent(event.id)
if (res.data) {
const updatedEvent = {
...event,
likes: res.data.likes,
isLiked: res.data.isLiked,
}
setLikes(updatedEvent.likes)
setIsLiked(updatedEvent.isLiked)
onLikeChange?.(updatedEvent)
}
} catch {
// Revert on error
setIsLiked((prev) => !prev)
setLikes((prev) => (isLiked ? prev + 1 : Math.max(0, prev - 1)))
} finally {
setLiking(false)
}
}
const openLightbox = (idx: number) => {
setLightboxIndex(idx)
setLightboxOpen(true)
}
const closeLightbox = () => setLightboxOpen(false)
return (
<>
<Dialog.Body className="flex flex-col min-h-0">
{/* Header */}
<div className="pb-3 pr-16 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>
<LikeButton
onClick={handleLike}
disabled={liking}
isLiked={isLiked}
likes={likes}
/>
</div>
</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>
</div>
</div>
{/* Scrollable body */}
<div className={`overflow-y-auto ${isMaximized ? 'flex-1 min-h-0' : 'max-h-[62vh]'}`}>
{/* Photo Gallery */}
{photos.length > 0 && (
<div className="m-3 border-b border-gray-200 dark:border-gray-700">
{/* Main photo */}
<div className="relative w-full rounded-lg overflow-hidden dark:bg-gray-900 mb-2 mx-auto">
<img
src={imageSrc(photos[activePhoto])}
alt={`${event.name} ${activePhoto + 1}`}
className="w-full h-56 object-contain cursor-zoom-in"
onClick={() => openLightbox(activePhoto)}
/>
<Button
onClick={() => openLightbox(activePhoto)}
variant="plain"
shape="none"
icon={<FaExpand className="h-4 w-4" />}
className="absolute top-2 right-2 !h-7 !w-7 !rounded-lg !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
title="Tam ekran"
/>
{photos.length > 1 && (
<>
<Button
onClick={(e) => {
e.stopPropagation()
setActivePhoto((i) => (i - 1 + photos.length) % photos.length)
}}
variant="plain"
shape="circle"
icon={<FaChevronLeft className="h-4 w-4" />}
className="absolute left-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
/>
<Button
onClick={(e) => {
e.stopPropagation()
setActivePhoto((i) => (i + 1) % photos.length)
}}
variant="plain"
shape="circle"
icon={<FaChevronRight className="h-4 w-4" />}
className="absolute right-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
/>
</>
)}
</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)}
variant="plain"
shape="none"
className={`flex-shrink-0 !h-16 !w-16 !rounded-lg !px-0 overflow-hidden border-2 transition-all ${
idx === activePhoto
? 'border-green-500 opacity-100'
: 'border-transparent opacity-60 hover:opacity-90'
}`}
>
<img
src={imageSrc(photo)}
alt={`Thumbnail ${idx + 1}`}
className="w-full h-full object-cover"
/>
</Button>
))}
</div>
)}
</div>
)}
{/* Description */}
{event.description && (
<div className="pb-2 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="py-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-2">
<FaCommentAlt className="w-4 h-4 text-green-500" />
{translate('::App.Intranet.Events.Comments')} ({comments.length})
</h3>
{/* Comment List */}
<div className="max-h-64 overflow-y-auto">
{comments.length === 0 ? (
<p className="text-sm text-gray-400 dark:text-gray-500 text-center">
{translate('::App.Intranet.Events.NoComment')}
</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>
</div>
</div>
</Dialog.Body>
<Dialog.Footer className="pt-2 border-t border-gray-200 dark:border-gray-700 flex-shrink-0 flex items-end gap-1">
<div className="flex flex-1 gap-2 items-end">
<textarea
ref={commentInputRef}
value={commentText}
autoFocus
onChange={(e) => setCommentText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={translate('::App.Intranet.Events.EventAttendance')}
rows={1}
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
/>
<Button
type="button"
size="sm"
onClick={handleSubmitComment}
disabled={!commentText.trim() || submitting}
variant="solid"
icon={<FaPaperPlane className="h-4 w-4" />}
/>
</div>
<Button
size="sm"
onClick={onClose}
type="button"
variant="solid"
>
{translate('::App.Platform.Close')}
</Button>
</Dialog.Footer>
<ImageLightbox
images={photos}
activeIndex={lightboxIndex}
isOpen={lightboxOpen}
altPrefix={event.name}
getSrc={imageSrc}
onClose={closeLightbox}
onChangeIndex={setLightboxIndex}
/>
</>
)
}
const EventModal: React.FC<EventModalProps> = ({ event, onClose, onLikeChange }) => {
return (
<Dialog isOpen onClose={onClose} onRequestClose={onClose} width={768}>
<EventModalContent event={event} onClose={onClose} onLikeChange={onLikeChange} />
</Dialog>
)
}
export default EventModal