'use client' import { useMemo, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import Button from '@/components/formElements/Button' import ConsumerInput from '@/components/consumer/ConsumerInput' import ConsumerModal from '@/components/consumer/ConsumerModal' import AddDocumentIcon from '@/components/icons/AddDocumentIcon' import HeatIconFill from '@/components/icons/HeatIconFill' import ReviewCard from '@/components/reviews/ReviewCard' import StarRating from '@/components/reviews/StarRating' import { coerceToString } from '@/helpers' import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics' import { isWithinHostReplyWindow, isWithinReviewWindow } from '@/lib/reviewWindow' import { addToast } from '@/lib/toast' import useAuth from '@/hooks/useAuth' import { useEventReviewsInfiniteQuery } from '@/queries/consumer/useEventDetailQueries' import { useEventViewerStateQuery } from '@/queries/consumer/useEventViewerStateQuery' import { consumerKeys } from '@/queries/consumerKeys' import { CREATE_REVIEW, HOST_REPLY_REVIEW, type EventReview } from '@/services/reviews' import { texts, format } from '@/texts' interface EventReviewsSectionProps { eventId: string eventStatus: string /** Scheduled event end — review window is endsAt + 1 month; host reply + 2 months. */ endsAt: string avgRating?: number | null reviewsCount?: number initialReviews?: EventReview[] reviewsTotal?: number /** Event organizer — used to enable host reply controls. */ organizerId?: string } const authorDisplayName = (review: EventReview) => { const name = [review.user?.firstName, review.user?.lastName].filter(Boolean).join(' ').trim() return name || texts.events.guest } const EventReviewsSection = ({ eventId, eventStatus, endsAt, avgRating: initialAvg, reviewsCount: initialCount, initialReviews, reviewsTotal: initialTotal, organizerId, }: EventReviewsSectionProps) => { const { user } = useAuth() const queryClient = useQueryClient() const viewerStateQuery = useEventViewerStateQuery(eventId) const [draftBody, setDraftBody] = useState('') const [draftRating, setDraftRating] = useState(0) const [isSaving, setIsSaving] = useState(false) const [replyTarget, setReplyTarget] = useState(null) const [replyBody, setReplyBody] = useState('') const [isSavingReply, setIsSavingReply] = useState(false) const isHost = Boolean(organizerId && user?.userId && user.userId === organizerId) const reviewsSeed = initialReviews !== undefined ? { items: initialReviews, totalItemsCount: initialTotal ?? initialCount ?? initialReviews.length } : undefined const reviewsQuery = useEventReviewsInfiniteQuery(eventId, reviewsSeed) const reviews = useMemo(() => { const uniqueReviews = new Map() for (const page of reviewsQuery.data?.pages ?? []) { for (const review of page.items) uniqueReviews.set(review.id, review) } return [...uniqueReviews.values()] }, [reviewsQuery.data?.pages]) const reviewsTotal = reviewsQuery.data?.pages.at(-1)?.totalItemsCount ?? initialTotal ?? initialCount ?? 0 const isLoading = reviewsQuery.isPending && reviewsQuery.fetchStatus === 'fetching' const isLoadingMore = reviewsQuery.isFetchingNextPage const bookingId = eventStatus === 'completed' && viewerStateQuery.data?.activeBooking?.status === 'confirmed' ? viewerStateQuery.data.activeBooking.id : null const myReview = reviews.find((review) => review.userId === user?.userId) ?? null const windowOpen = isWithinReviewWindow(endsAt) const canReview = eventStatus === 'completed' && !!bookingId && !myReview && !!user?.userId && windowOpen const showReviewWindowExpired = eventStatus === 'completed' && !!bookingId && !myReview && !!user?.userId && !windowOpen const replyWindowOpen = isWithinHostReplyWindow(endsAt) const canHostReply = isHost && replyWindowOpen const avgRating = initialAvg ?? (reviews.length > 0 ? Math.round((reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length) * 10) / 10 : null) const reviewsCount = reviewsTotal const reloadReviews = () => { void queryClient.invalidateQueries({ queryKey: consumerKeys.eventReviewsRoot(eventId) }) void queryClient.invalidateQueries({ queryKey: consumerKeys.eventDetailRoot(eventId) }) } const handleSubmitReview = async () => { if (!bookingId) return if (draftRating < 1 || draftRating > 5) { addToast({ title: texts.reviews.selectRating, color: 'warning' }) return } setIsSaving(true) const trimmed = draftBody.trim() const result = await CREATE_REVIEW(eventId, { bookingId, rating: draftRating, body: trimmed.length > 0 ? trimmed : null, }) setIsSaving(false) if (!result.ok) return trackAnalyticsEventOnce(ANALYTICS_EVENTS.REVIEW_SUBMITTED, result.data.id, { review_id: result.data.id, event_id: eventId, booking_id: bookingId, rating: draftRating, has_comment: trimmed.length > 0, }) addToast({ title: texts.reviews.submitted, color: 'success' }) setDraftBody('') setDraftRating(0) reloadReviews() } 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 } setIsSavingReply(true) const result = await HOST_REPLY_REVIEW(replyTarget.id, { hostReplyBody: trimmed }) setIsSavingReply(false) if (!result.ok) return addToast({ title: texts.reviews.replySaved, color: 'success' }) setReplyTarget(null) reloadReviews() } const clearReply = async () => { if (!replyTarget) return setIsSavingReply(true) const result = await HOST_REPLY_REVIEW(replyTarget.id, { hostReplyBody: null }) setIsSavingReply(false) if (!result.ok) return addToast({ title: texts.reviews.replyDeleted, color: 'success' }) setReplyTarget(null) reloadReviews() } return (

{texts.reviews.peopleSayTitle}

{reviewsCount > 0 && avgRating != null ? (
{format(texts.reviews.avgFromCount, { rating: avgRating.toLocaleString('fa-IR', { maximumFractionDigits: 1 }), count: reviewsCount.toLocaleString('fa-IR'), })}
) : null}
{showReviewWindowExpired ? (

{texts.errors.codes.REVIEW_WINDOW_EXPIRED}

) : null} {isLoading ?

{texts.reviews.loading}

: null} {!isLoading && reviews.length === 0 ? (

{texts.reviews.eventEmptyTitle}

{texts.reviews.eventEmptyHint}

) : null} {!isLoading && reviews.length > 0 ? (
    {reviews.map((review, index) => ( { openReply(review) }} > {review.hostReplyBody ? texts.reviews.editReply : texts.common.reply} ) : null } hostReplyBody={review.hostReplyBody} rating={review.rating} tone="consumer" /> ))}
) : null} {!isLoading && reviewsQuery.hasNextPage ? ( ) : null} {canReview ? (

{texts.reviews.shareExperienceTitle}

{ setDraftBody(coerceToString(next)) }} />
) : null} {isHost && !replyWindowOpen && reviews.length > 0 ? (

{texts.errors.codes.HOST_REPLY_WINDOW_EXPIRED}

) : null} void clearReply()} > {texts.reviews.deleteReply} ) : null } isLoading={isSavingReply} 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)) }} />
) } export default EventReviewsSection