Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
110 lines
2.9 KiB
TypeScript
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
|