import { useCallback, useEffect, useMemo, useState } from 'react'; import { getSikshyaApi, SIKSHYA_ENDPOINTS } from '../api'; import { GatedFeatureWorkspace } from '../components/GatedFeatureWorkspace'; import { ApiErrorPanel } from '../components/shared/ApiErrorPanel'; import { StatusBadge } from '../components/shared/list/StatusBadge'; import { ButtonPrimary, ButtonSecondary } from '../components/shared/buttons'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { useSikshyaDialog } from '../components/shared/SikshyaDialogContext'; import { useAsyncData } from '../hooks/useAsyncData'; import { useAddonEnabled } from '../hooks/useAddons'; import { isFeatureEnabled, resolveGatedWorkspaceMode } from '../lib/licensing'; import { useAdminRouting } from '../lib/adminRouting'; import { formatPostDate } from '../lib/formatPostDate'; import type { SikshyaReactConfig } from '../types'; import { __ } from '../lib/i18n'; type ReviewRow = { id: number; user_id: number; course_id: number; rating: number; review_text: string; is_approved: boolean; author_name: string; author_email: string; course_title: string; created_at: string; created_at_label: string; edit_url: string; view_url: string; reported_count?: number; last_reported_at?: string; reply_text?: string; reply_user_id?: number; reply_created_at?: string; }; type DetailResponse = { success: boolean; data?: ReviewRow; message?: string }; function StarDisplay({ value }: { value: number }) { const v = Math.max(0, Math.min(5, Math.round(value))); return ( {[1, 2, 3, 4, 5].map((n) => ( {n <= v ? '★' : '☆'} ))} ); } export function ReviewDetailPage(props: { embedded?: boolean; config: SikshyaReactConfig; title: string }) { const { config, title } = props; const { confirm } = useSikshyaDialog(); const { route, navigateView } = useAdminRouting(); const featureOk = isFeatureEnabled(config, 'course_reviews'); const addon = useAddonEnabled('course_reviews'); const mode = resolveGatedWorkspaceMode(featureOk, addon.enabled, addon.loading); const gateOpen = mode === 'full'; const reviewId = useMemo(() => parseInt(route.query?.id || '0', 10) || 0, [route.query?.id]); const [busy, setBusy] = useState(false); const [replyDraft, setReplyDraft] = useState(''); const loader = useCallback(async () => { if (!reviewId) { throw new Error(__('Missing review id.', 'sikshya')); } if (!gateOpen) { return null; } return getSikshyaApi().get(SIKSHYA_ENDPOINTS.admin.review(reviewId)); }, [gateOpen, reviewId]); const { loading, data, error, refetch } = useAsyncData(loader, [gateOpen, reviewId]); const row = gateOpen && data && data.success && data.data ? data.data : null; useEffect(() => { if (row) { setReplyDraft(row.reply_text ?? ''); } }, [row?.id, row?.reply_text]); const saveReply = async () => { const text = replyDraft.trim(); if (!text || !reviewId) return; setBusy(true); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.reviewReply(reviewId), { reply_text: text }); await refetch(); } finally { setBusy(false); } }; const removeReply = async () => { if (!reviewId) return; const ok = await confirm({ title: __('Remove reply?', 'sikshya'), message: __('This removes the public instructor/admin reply from the course page.', 'sikshya'), variant: 'danger', confirmLabel: __('Remove', 'sikshya'), }); if (!ok) return; setBusy(true); try { await getSikshyaApi().delete(SIKSHYA_ENDPOINTS.admin.reviewReply(reviewId)); setReplyDraft(''); await refetch(); } finally { setBusy(false); } }; const approve = async () => { if (!reviewId) return; setBusy(true); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.reviewApprove(reviewId), {}); await refetch(); } finally { setBusy(false); } }; const unpublish = async () => { if (!reviewId) return; setBusy(true); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.reviewReject(reviewId), {}); await refetch(); } finally { setBusy(false); } }; const remove = async () => { if (!reviewId) return; const ok = await confirm({ title: __('Delete review?', 'sikshya'), message: __('This review will be permanently removed and the course rating recalculated.', 'sikshya'), variant: 'danger', confirmLabel: __('Delete', 'sikshya'), }); if (!ok) return; setBusy(true); try { await getSikshyaApi().delete(SIKSHYA_ENDPOINTS.admin.reviewDelete(reviewId)); navigateView('reviews', {}, { replace: true }); } finally { setBusy(false); } }; return ( navigateView('reviews')}> ← Back to reviews } > addon.enable()} addonError={addon.error} > {!gateOpen ? null : error ? ( refetch()} /> ) : loading ? (
{__('Loading…', 'sikshya')}
) : !reviewId ? (

{__('Missing review id.', 'sikshya')}

) : row ? (
{row.rating > 0 ? : }

Submitted {formatPostDate(row.created_at)} · {row.created_at_label}

{row.author_name || `User #${row.user_id}`} {row.author_email ? ( {row.author_email} ) : null}

{__('Course:', 'sikshya')} {row.view_url ? ( {row.course_title || `#${row.course_id}`} ) : ( row.course_title || `#${row.course_id}` )}

Reports: {Number(row.reported_count ?? 0).toLocaleString()} {row.last_reported_at ? ( · Last {formatPostDate(row.last_reported_at)} ) : null}

{row.is_approved ? ( void unpublish()}> Unpublish ) : ( void approve()}> Approve )} void remove()} > Delete

{__('Review', 'sikshya')}

{row.review_text ? (

{row.review_text}

) : (

(Rating only — no written review)

)}

{__('Official reply', 'sikshya')}

Shown publicly under this review on the course page.