admin/components/reviews/ReviewModal.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

110 lines
2.9 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { coerceToString } from '@/helpers'
import { addToast } from '@/lib/toast'
import ConsumerInput from '@/components/consumer/ConsumerInput'
import ConsumerModal from '@/components/consumer/ConsumerModal'
import StarRating from '@/components/reviews/StarRating'
import { CREATE_REVIEW, type EventReview } from '@/services/reviews'
import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics'
import { texts } from '@/texts'
interface ReviewModalProps {
isOpen: boolean
onOpenChange: (open: boolean) => void
eventId: string
bookingId: string
onSaved?: (review: EventReview) => void
}
/** Create-only review modal (booking surfaces). Event detail uses inline compose instead. */
const ReviewModal = ({ isOpen, onOpenChange, eventId, bookingId, onSaved }: ReviewModalProps) => {
const [rating, setRating] = useState(0)
const [body, setBody] = useState('')
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (!isOpen) return
setRating(0)
setBody('')
}, [isOpen])
const handleSave = async () => {
if (rating < 1 || rating > 5) {
addToast({ title: texts.reviews.selectRating, color: 'warning' })
return
}
setIsSaving(true)
const trimmed = body.trim()
const result = await CREATE_REVIEW(eventId, {
bookingId,
rating,
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,
has_comment: trimmed.length > 0,
})
addToast({
title: texts.reviews.submitted,
color: 'success',
})
onSaved?.(result.data)
onOpenChange(false)
}
return (
<ConsumerModal
acceptBtnDisabled={rating < 1 || isSaving}
acceptBtnText={texts.reviews.submitReview}
isLoading={isSaving}
isOpen={isOpen}
rejectBtnText={texts.common.cancel}
size="md"
title={texts.reviews.createTitle}
onAccept={() => void handleSave()}
onOpenChange={onOpenChange}
onReject={() => {
onOpenChange(false)
}}
>
<div className="space-y-4">
<div>
<p className="mb-2 text-sm text-secondary-20">{texts.reviews.yourRating}</p>
<StarRating
value={rating}
onChange={setRating}
/>
</div>
<ConsumerInput
generalType="textarea"
label={texts.reviews.bodyLabel}
name="reviewBody"
placeholder={texts.reviews.bodyPlaceholder}
textAreaMinRows={3}
value={body}
onValueChange={(next) => {
setBody(coerceToString(next))
}}
/>
<p className="text-xs text-default-400">{texts.reviews.ratingOnlyHint}</p>
</div>
</ConsumerModal>
)
}
export default ReviewModal