feat:migliorato pulsanti voto per poter votare piu volte

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Diego-C-05
2026-05-06 12:42:47 +02:00
parent 358e498837
commit e7d6cab186
3 changed files with 95 additions and 23 deletions
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import MagicCard from './MagicCard'
import { fetchPersonalizedFeed } from '../services/feedService'
import { sendFeedback } from '../services/feedbackService'
import type { Article } from '../types/article'
type FeedContentProps = {
@@ -11,6 +12,46 @@ type FeedContentProps = {
function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedContentProps) {
const [articles, setArticles] = useState<Article[]>([])
const [status, setStatus] = useState<'loading' | 'ok' | 'error'>('loading')
const [voteByArticle, setVoteByArticle] = useState<Record<string, 1 | -1 | null>>({})
const [pendingByArticle, setPendingByArticle] = useState<Record<string, boolean>>({})
const [voteError, setVoteError] = useState<string | null>(null)
const sendVoteDelta = async (articleId: string, previousVote: 1 | -1 | null, nextVote: 1 | -1 | null) => {
// Without server idempotency, simulate undo by applying compensating vote.
if (previousVote === nextVote) return
if (previousVote === null && nextVote !== null) {
await sendFeedback(articleId, nextVote)
return
}
if (previousVote !== null && nextVote === null) {
await sendFeedback(articleId, previousVote === 1 ? -1 : 1)
return
}
if (previousVote !== null && nextVote !== null) {
await sendFeedback(articleId, nextVote)
}
}
const handleVoteChange = async (articleId: string, nextVote: 1 | -1 | null) => {
const previousVote = voteByArticle[articleId] ?? null
if (previousVote === nextVote || pendingByArticle[articleId]) return
setVoteError(null)
setVoteByArticle((prev) => ({ ...prev, [articleId]: nextVote }))
setPendingByArticle((prev) => ({ ...prev, [articleId]: true }))
try {
await sendVoteDelta(articleId, previousVote, nextVote)
} catch {
setVoteByArticle((prev) => ({ ...prev, [articleId]: previousVote }))
setVoteError('Impossibile aggiornare il voto. Riprova.')
} finally {
setPendingByArticle((prev) => ({ ...prev, [articleId]: false }))
}
}
// Prende il fetch del feed personalizzato e popola la variabile di stato con i dati
useEffect(() => {fetchPersonalizedFeed().then((data) => {
@@ -30,6 +71,7 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
{/* Gestione degli stati*/}
{status === 'loading' && <p>Caricamento feed...</p>}
{status === 'error' && <p>Errore nel caricamento del feed.</p>}
{voteError && <p className="feed-no-results">{voteError}</p>}
{/*Render della lista di articoli se il caricamento è andato a buon fine*/}
{status === 'ok' && (
<>
@@ -78,6 +120,9 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
summary={a.summary}
tags={a.macroTopics?.length ? a.macroTopics : (a.trendingTopics || [a.category])}
entities={a.entities || []}
voteState={voteByArticle[a.uniqueKey] ?? null}
votePending={pendingByArticle[a.uniqueKey] ?? false}
onVoteChange={handleVoteChange}
/>
))}
</div>
+12 -12
View File
@@ -1,6 +1,4 @@
import type { ReactNode } from 'react'
import { useState } from 'react'
import { sendFeedback } from '../services/feedbackService'
type MagicCardProps = {
articleId?: string
@@ -12,16 +10,18 @@ type MagicCardProps = {
summary: string
tags: string[]
entities: string[]
voteState: 1 | -1 | null
votePending?: boolean
onVoteChange: (articleId: string, nextVote: 1 | -1 | null) => void
}
// Card notizia: mostra fonte, sentiment, testo, tag, entità e azioni rapide.
function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, summary, tags, entities }: MagicCardProps) {
const [voted, setVoted] = useState<1 | -1 | null>(null)
function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, summary, tags, entities, voteState, votePending = false, onVoteChange }: MagicCardProps) {
const handleVote = (clickedVote: 1 | -1) => {
if (!articleId || votePending) return
const handleVote = (vote: 1 | -1) => {
if (voted !== null) return
setVoted(vote)
if (articleId) sendFeedback(articleId, vote)
const nextVote = voteState === clickedVote ? null : clickedVote
onVoteChange(articleId, nextVote)
}
return (
@@ -79,8 +79,8 @@ function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, s
label="Mi piace"
tone="like"
onClick={() => handleVote(1)}
disabled={voted !== null}
style={{ opacity: voted === -1 ? 0.4 : 1 }}
disabled={votePending}
style={{ opacity: voteState === -1 ? 0.4 : 1 }}
>
<ThumbUpIcon />
</ActionButton>
@@ -88,8 +88,8 @@ function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, s
label="Non mi piace"
tone="dislike"
onClick={() => handleVote(-1)}
disabled={voted !== null}
style={{ opacity: voted === 1 ? 0.4 : 1 }}
disabled={votePending}
style={{ opacity: voteState === 1 ? 0.4 : 1 }}
>
<ThumbDownIcon />
</ActionButton>
@@ -1,21 +1,48 @@
const N8N = import.meta.env.VITE_N8N_URL
const FEEDBACK_TIMEOUT_MS = 5000
export const sendFeedback = async (
articleId: string,
vote: 1 | -1
): Promise<void> => {
const token = localStorage.getItem('briefai_token')
if (!token) return
if (!token) throw new Error('Token mancante')
const payload = JSON.parse(atob(token.split('.')[1]))
if (!N8N) throw new Error('VITE_N8N_URL non configurata')
await fetch(`${N8N}/briefai/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: payload.userId,
articleId,
vote,
}),
})
let payload: { userId?: string } | null = null
try {
payload = JSON.parse(atob(token.split('.')[1]))
} catch {
throw new Error('Token non valido')
}
if (!payload?.userId) throw new Error('userId non presente nel token')
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), FEEDBACK_TIMEOUT_MS)
try {
const response = await fetch(`${N8N}/briefai/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: payload.userId,
articleId,
vote,
}),
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`Feedback HTTP ${response.status}`)
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Timeout feedback')
}
throw error
} finally {
clearTimeout(timeoutId)
}
}