'use client' import { useCallback, useEffect, useState } from 'react' import { coerceToString } from '@/helpers' import { addToast } from '@/lib/toast' import Button from '@/components/formElements/Button' import Input from '@/components/formElements/Input' import AdminState from '@/components/feedback/AdminState' import ConsumerState from '@/components/feedback/ConsumerState' import ConsumerModal from '@/components/consumer/ConsumerModal' import ReviewCard from '@/components/reviews/ReviewCard' import StatusChip from '@/components/ui/StatusChip' import { getReviewStatus } from '@/constants/status' import { isWithinHostReplyWindow } from '@/lib/reviewWindow' import { EVENT_REVIEWS_PAGE_SIZE, HOST_REPLY_REVIEW, LIST_ADMIN_EVENT_REVIEWS_PAGE, LIST_EVENT_REVIEWS_PAGE, type EventReview, } from '@/services/reviews' import { texts } from '@/texts' interface EventReviewsPanelProps { eventId: string /** * Scheduled event end — host reply window is endsAt + 2 months. * Required for host mode; unused when mode is admin (read-only). */ endsAt?: string /** Host can reply; admin only views reviews + host replies. */ mode?: 'host' | 'admin' } const authorDisplayName = (review: EventReview) => { const name = [review.user?.firstName, review.user?.lastName].filter(Boolean).join(' ').trim() return name || texts.events.guest } const EventReviewsPanel = ({ eventId, endsAt, mode = 'host' }: EventReviewsPanelProps) => { const isAdmin = mode === 'admin' const [reviews, setReviews] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isLoadingMore, setIsLoadingMore] = useState(false) const [page, setPage] = useState(1) const [totalItems, setTotalItems] = useState(0) const [replyTarget, setReplyTarget] = useState(null) const [replyBody, setReplyBody] = useState('') const [isSaving, setIsSaving] = useState(false) const replyWindowOpen = !isAdmin && !!endsAt && isWithinHostReplyWindow(endsAt) const fetchPage = useCallback( async (nextPage: number, options?: { errorMode: 'silent' }) => { if (isAdmin) { return LIST_ADMIN_EVENT_REVIEWS_PAGE(eventId, nextPage, EVENT_REVIEWS_PAGE_SIZE, options) } return LIST_EVENT_REVIEWS_PAGE(eventId, nextPage, EVENT_REVIEWS_PAGE_SIZE, options) }, [eventId, isAdmin] ) const load = useCallback(async () => { setIsLoading(true) const result = await fetchPage(1) if (result.ok) { setReviews(result.data.items) setTotalItems(result.data.totalItemsCount) setPage(1) } setIsLoading(false) }, [fetchPage]) const loadMore = async () => { if (isLoadingMore || reviews.length >= totalItems) return setIsLoadingMore(true) const nextPage = page + 1 const result = await fetchPage(nextPage, { errorMode: 'silent' }) if (result.ok) { setReviews((current) => { const seen = new Set(current.map((review) => review.id)) return [...current, ...result.data.items.filter((review) => !seen.has(review.id))] }) setPage(nextPage) } setIsLoadingMore(false) } useEffect(() => { void load() }, [load]) const openReply = (review: EventReview) => { if (!replyWindowOpen) { addToast({ title: texts.errors.codes.HOST_REPLY_WINDOW_EXPIRED, color: 'warning' }) return } setReplyTarget(review) setReplyBody(review.hostReplyBody ?? '') } const saveReply = async () => { if (!replyTarget) return const trimmed = replyBody.trim() if (!trimmed) { addToast({ title: texts.reviews.replyBodyRequired, color: 'warning' }) return } setIsSaving(true) const result = await HOST_REPLY_REVIEW(replyTarget.id, { hostReplyBody: trimmed }) setIsSaving(false) if (!result.ok) return addToast({ title: texts.reviews.replySaved, color: 'success' }) setReplyTarget(null) setReviews((current) => current.map((review) => (review.id === result.data.id ? result.data : review))) } const clearReply = async () => { if (!replyTarget) return setIsSaving(true) const result = await HOST_REPLY_REVIEW(replyTarget.id, { hostReplyBody: null }) setIsSaving(false) if (!result.ok) return addToast({ title: texts.reviews.replyDeleted, color: 'success' }) setReplyTarget(null) setReviews((current) => current.map((review) => (review.id === result.data.id ? result.data : review))) } if (isLoading) { return

{texts.reviews.loading}

} if (reviews.length === 0) { return (
{isAdmin ? ( ) : ( )}
) } return (
{!isAdmin && !replyWindowOpen ? (

{texts.errors.codes.HOST_REPLY_WINDOW_EXPIRED}

) : null}
    {reviews.map((review) => { const statusPresentation = isAdmin ? getReviewStatus(review.status) : null return ( { openReply(review) }} > {review.hostReplyBody ? texts.reviews.editReply : texts.common.reply} ) : statusPresentation ? (
    ) : null } hostReplyBody={review.hostReplyBody} rating={review.rating} tone={isAdmin ? 'organizer' : 'consumer'} /> ) })}
{reviews.length < totalItems ? (
) : null} {!isAdmin ? ( void clearReply()} > {texts.reviews.deleteReply} ) : null } isLoading={isSaving} isOpen={!!replyTarget} rejectBtnText={texts.common.close} title={texts.reviews.replyModalTitle} onAccept={() => void saveReply()} onOpenChange={(open) => { if (!open) setReplyTarget(null) }} onReject={() => { setReplyTarget(null) }} > { setReplyBody(coerceToString(next)) }} /> ) : null}
) } export default EventReviewsPanel