import { useCallback, useEffect, useState } from 'react' import type { Review, Article } from '@/types/nostr' import { getReviewsForArticle } from '@/lib/reviews' import { getReviewTipsForArticle } from '@/lib/reviewAggregation' import { Card, ErrorState, Button } from './ui' import { ReviewForm } from './ReviewForm' import { ReviewTipForm } from './ReviewTipForm' import { t } from '@/lib/i18n' interface ArticleReviewsProps { article: Article authorPubkey: string } export function ArticleReviews({ article, authorPubkey }: ArticleReviewsProps): React.ReactElement { const data = useArticleReviewsData({ articleId: article.id, authorPubkey }) const reviewForm = useReviewFormState({ reload: data.reload }) const tipSelection = useReviewTipSelection({ article, reviews: data.reviews, reload: data.reload }) return ( {reviewForm.show && ( )} {data.loading && {t('common.loading')}} {data.error && } {!data.loading && !data.error && data.reviews.length === 0 && !reviewForm.show && ( {t('review.empty')} )} {!data.loading && !data.error && } ) } interface ReviewTipSelectionController { article: Article selectedReviewForTip: Review | null onTipSuccess: () => void clear: () => void select: (reviewId: string) => void } interface ArticleReviewsData { reviews: Review[] tips: number loading: boolean error: string | null reload: () => Promise } function useArticleReviewsData({ articleId, authorPubkey, }: { articleId: string authorPubkey: string }): ArticleReviewsData { const [reviews, setReviews] = useState([]) const [tips, setTips] = useState(0) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const reload = useCallback(async (): Promise => { setLoading(true) setError(null) try { const [list, tipsTotal] = await Promise.all([ getReviewsForArticle(articleId), getReviewTipsForArticle({ authorPubkey, articleId }), ]) setReviews(list) setTips(tipsTotal) } catch (loadError) { setError(loadError instanceof Error ? loadError.message : 'Erreur lors du chargement des critiques') } finally { setLoading(false) } }, [articleId, authorPubkey]) useEffect(() => { void reload() }, [reload]) return { reviews, tips, loading, error, reload, } } function useReviewFormState({ reload }: { reload: () => Promise }): { show: boolean open: () => void close: () => void onSuccess: () => void } { const [show, setShow] = useState(false) const open = useCallback((): void => setShow(true), []) const close = useCallback((): void => setShow(false), []) const onSuccess = useCallback((): void => { close() void reload() }, [close, reload]) return { show, open, close, onSuccess } } function useReviewTipSelection({ article, reviews, reload, }: { article: Article reviews: Review[] reload: () => Promise }): ReviewTipSelectionController { const [selectedReviewId, setSelectedReviewId] = useState(null) const select = useCallback((reviewId: string): void => setSelectedReviewId(reviewId), []) const clear = useCallback((): void => setSelectedReviewId(null), []) const onTipSuccess = useCallback((): void => { clear() void reload() }, [clear, reload]) const selectedReviewForTip = selectedReviewId ? findReviewById(reviews, selectedReviewId) : null return { article, selectedReviewForTip, onTipSuccess, clear, select } } function findReviewById(reviews: Review[], reviewId: string): Review | null { const review = reviews.find((r) => r.id === reviewId) return review ?? null } function SelectedReviewTipForm({ selection }: { selection: ReviewTipSelectionController }): React.ReactElement | null { if (!selection.selectedReviewForTip) { return null } return ( ) } function ArticleReviewsHeader({ tips, onAddReview }: { tips: number; onAddReview: () => void }): React.ReactElement { return ( {t('review.title')} {t('review.tips.total', { amount: tips })} {t('review.add')} ) } function ArticleReviewsList({ reviews, onTipReview }: { reviews: Review[]; onTipReview: (reviewId: string) => void }): React.ReactElement { return ( {reviews.map((r) => ( {r.title && ( {r.title} )} {r.content} {r.text && ( {r.text} )} {t('review.reviewer')}: {formatPubkey(r.reviewerPubkey)} • {formatDate(r.createdAt)} { onTipReview(r.id) }} className="ml-auto" > {t('review.tip.button')} ))} ) } function formatPubkey(pubkey: string): string { return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}` } function formatDate(timestamp: number): string { return new Date(timestamp * 1000).toLocaleString() }
{t('common.loading')}
{t('review.empty')}