story-research-zapwall/components/ArticleReviews.tsx
2026-01-06 14:17:55 +01:00

142 lines
4.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import type { Review, Article } from '@/types/nostr'
import { getReviewsForArticle } from '@/lib/reviews'
import { getReviewTipsForArticle } from '@/lib/reviewAggregation'
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 [reviews, setReviews] = useState<Review[]>([])
const [tips, setTips] = useState<number>(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [showReviewForm, setShowReviewForm] = useState(false)
const [selectedReviewForTip, setSelectedReviewForTip] = useState<string | null>(null)
const loadReviews = async (): Promise<void> => {
setLoading(true)
setError(null)
try {
const [list, tipsTotal] = await Promise.all([
getReviewsForArticle(article.id),
getReviewTipsForArticle({ authorPubkey, articleId: article.id }),
])
setReviews(list)
setTips(tipsTotal)
} catch (e) {
setError(e instanceof Error ? e.message : 'Erreur lors du chargement des critiques')
} finally {
setLoading(false)
}
}
useEffect(() => {
void loadReviews()
}, [article.id, authorPubkey])
return (
<div className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark space-y-4">
<ArticleReviewsHeader tips={tips} onAddReview={() => {
setShowReviewForm(true)
}} />
{showReviewForm && (
<ReviewForm
article={article}
onSuccess={() => {
setShowReviewForm(false)
void loadReviews()
}}
onCancel={() => {
setShowReviewForm(false)
}}
/>
)}
{loading && <p className="text-sm text-cyber-accent">{t('common.loading')}</p>}
{error && <p className="text-sm text-red-400">{error}</p>}
{!loading && !error && reviews.length === 0 && !showReviewForm && (
<p className="text-sm text-cyber-accent/70">{t('review.empty')}</p>
)}
{!loading && !error && <ArticleReviewsList reviews={reviews} onTipReview={(reviewId) => {
setSelectedReviewForTip(reviewId)
}} />}
{selectedReviewForTip && (
<ReviewTipForm
review={reviews.find((r) => r.id === selectedReviewForTip)!}
article={article}
onSuccess={() => {
setSelectedReviewForTip(null)
void loadReviews()
}}
onCancel={() => {
setSelectedReviewForTip(null)
}}
/>
)}
</div>
)
}
function ArticleReviewsHeader({ tips, onAddReview }: { tips: number; onAddReview: () => void }): React.ReactElement {
return (
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-neon-cyan">{t('review.title')}</h3>
<div className="flex items-center gap-4">
<span className="text-sm text-cyber-accent/70">{t('review.tips.total', { amount: tips })}</span>
<button
onClick={onAddReview}
className="px-3 py-1 text-sm bg-neon-green/20 hover:bg-neon-green/30 text-neon-green rounded-lg font-medium transition-all border border-neon-green/50"
>
{t('review.add')}
</button>
</div>
</div>
)
}
function ArticleReviewsList({ reviews, onTipReview }: { reviews: Review[]; onTipReview: (reviewId: string) => void }): React.ReactElement {
return (
<div className="space-y-4">
{reviews.map((r) => (
<div key={r.id} className="border-t border-neon-cyan/20 pt-4 space-y-2">
{r.title && (
<h4 className="font-semibold text-neon-cyan">{r.title}</h4>
)}
<div className="text-cyber-accent whitespace-pre-wrap">{r.content}</div>
{r.text && (
<div className="text-sm text-cyber-accent/70 italic border-l-2 border-neon-cyan/30 pl-3">
{r.text}
</div>
)}
<div className="text-xs text-cyber-accent/50 flex gap-2 items-center">
<span>{t('review.reviewer')}: {formatPubkey(r.reviewerPubkey)}</span>
<span></span>
<span>{formatDate(r.createdAt)}</span>
<button
onClick={() => {
onTipReview(r.id)
}}
className="ml-auto px-2 py-1 text-xs bg-neon-green/20 hover:bg-neon-green/30 text-neon-green rounded transition-all border border-neon-green/50"
>
{t('review.tip.button')}
</button>
</div>
</div>
))}
</div>
)
}
function formatPubkey(pubkey: string): string {
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`
}
function formatDate(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleString()
}