(null)
+
+ // Carica articoli salvati e voti da localStorage
+ useEffect(() => {
+ try {
+ const saved = JSON.parse(localStorage.getItem('briefai_saved_articles') || '{}')
+ setSavedByArticle(saved)
+ const votes = JSON.parse(localStorage.getItem('briefai_voted_articles') || '{}')
+ setVoteByArticle(votes)
+ } catch (e) {
+ console.warn('Errore caricamento preferenze:', e)
+ }
+ }, [])
+
+ // Persisti voti in localStorage quando cambiano
+ useEffect(() => {
+ try {
+ localStorage.setItem('briefai_voted_articles', JSON.stringify(voteByArticle))
+ } catch (e) {
+ console.warn('Errore salvataggio voti:', e)
+ }
+ }, [voteByArticle])
const sendVoteDelta = async (articleId: string, previousVote: 1 | -1 | null, nextVote: 1 | -1 | null) => {
// Without server idempotency, simulate undo by applying compensating vote.
@@ -53,6 +78,23 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
}
}
+ const handleSaveArticle = async (articleId: string) => {
+ if (savePendingByArticle[articleId]) return
+
+ setSaveError(null)
+ setSavedByArticle((prev) => ({ ...prev, [articleId]: !prev[articleId] }))
+ setSavePendingByArticle((prev) => ({ ...prev, [articleId]: true }))
+
+ try {
+ await saveArticle(articleId)
+ } catch {
+ setSavedByArticle((prev) => ({ ...prev, [articleId]: !prev[articleId] }))
+ setSaveError('Impossibile salvare l\'articolo. Riprova.')
+ } finally {
+ setSavePendingByArticle((prev) => ({ ...prev, [articleId]: false }))
+ }
+ }
+
// Prende il fetch del feed personalizzato e popola la variabile di stato con i dati
useEffect(() => {fetchPersonalizedFeed().then((data) => {
setArticles(data)
@@ -72,6 +114,7 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
{status === 'loading' && Caricamento feed...
}
{status === 'error' && Errore nel caricamento del feed.
}
{voteError && {voteError}
}
+ {saveError && {saveError}
}
{/*Render della lista di articoli se il caricamento è andato a buon fine*/}
{status === 'ok' && (
<>
@@ -79,13 +122,13 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
{(() => {
const matchesTopic = (a: Article) => {
if (!topicsFilter || topicsFilter === 'All Topics') return true
- const topic = topicsFilter.toLowerCase()
- const catMatch = (a.category || '').toLowerCase() === topic
+ const topic = topicsFilter.toLowerCase().trim()
+ const catMatch = (a.category || '').toLowerCase().trim() === topic
const trending = (a.trendingTopics || []).some((t) =>
- String(t || '').toLowerCase() === topic
+ String(t || '').toLowerCase().trim() === topic
)
const macroTopicMatch = (a.macroTopics || []).some((t) =>
- String(t || '').toLowerCase() === topic
+ String(t || '').toLowerCase().trim() === topic
)
return catMatch || trending || macroTopicMatch
}
@@ -95,7 +138,18 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
return a.sentiment === sentimentFilter
}
- const filtered = articles.filter((a) => matchesSentiment(a) && matchesTopic(a))
+ const matchesPreference = (a: Article) => {
+ if (!preferenceFilter || preferenceFilter === 'Tutti') return true
+ if (preferenceFilter === 'Like') {
+ return voteByArticle[a.uniqueKey] === 1
+ }
+ if (preferenceFilter === 'Salvati') {
+ return savedByArticle[a.uniqueKey] === true
+ }
+ return true
+ }
+
+ const filtered = articles.filter((a) => matchesSentiment(a) && matchesTopic(a) && matchesPreference(a))
return (
@@ -122,7 +176,10 @@ function FeedContent({ sentimentFilter = null, topicsFilter = null }: FeedConten
entities={a.entities || []}
voteState={voteByArticle[a.uniqueKey] ?? null}
votePending={pendingByArticle[a.uniqueKey] ?? false}
+ isSaved={savedByArticle[a.uniqueKey] ?? false}
+ savePending={savePendingByArticle[a.uniqueKey] ?? false}
onVoteChange={handleVoteChange}
+ onSave={handleSaveArticle}
/>
))}
diff --git a/frontend-BriefAI/src/components/FeedTopbar.tsx b/frontend-BriefAI/src/components/FeedTopbar.tsx
index d918008..4bb9a0a 100644
--- a/frontend-BriefAI/src/components/FeedTopbar.tsx
+++ b/frontend-BriefAI/src/components/FeedTopbar.tsx
@@ -4,6 +4,8 @@ type FeedTopbarProps = {
onSentimentChange?: (sentiment: string) => void
topicsFilter?: string | null
onTopicChange?: (topic: string) => void
+ preferenceFilter?: string | null
+ onPreferenceChange?: (preference: string) => void
}
function FeedTopbar({
@@ -12,6 +14,8 @@ function FeedTopbar({
onSentimentChange,
topicsFilter = null,
onTopicChange,
+ preferenceFilter = null,
+ onPreferenceChange,
}: FeedTopbarProps) {
return (
Trasporti & Mobilità
+
+
) : null}
diff --git a/frontend-BriefAI/src/components/MagicCard.tsx b/frontend-BriefAI/src/components/MagicCard.tsx
index 9e55d3f..1303ca7 100644
--- a/frontend-BriefAI/src/components/MagicCard.tsx
+++ b/frontend-BriefAI/src/components/MagicCard.tsx
@@ -12,11 +12,14 @@ type MagicCardProps = {
entities: string[]
voteState: 1 | -1 | null
votePending?: boolean
+ isSaved?: boolean
+ savePending?: boolean
onVoteChange: (articleId: string, nextVote: 1 | -1 | null) => void
+ onSave?: (articleId: string) => void
}
// Card notizia: mostra fonte, sentiment, testo, tag, entità e azioni rapide.
-function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, summary, tags, entities, voteState, votePending = false, onVoteChange }: MagicCardProps) {
+function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, summary, tags, entities, voteState, votePending = false, isSaved = false, savePending = false, onVoteChange, onSave }: MagicCardProps) {
const handleVote = (clickedVote: 1 | -1) => {
if (!articleId || votePending) return
@@ -24,6 +27,11 @@ function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, s
onVoteChange(articleId, nextVote)
}
+ const handleSave = () => {
+ if (!articleId || savePending || !onSave) return
+ onSave(articleId)
+ }
+
return (
{/* Testata card: fonte e badge del sentiment allineati ai lati opposti. */}
@@ -78,22 +86,22 @@ function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, s
handleVote(1)}
disabled={votePending}
- style={{ opacity: voteState === -1 ? 0.4 : 1 }}
>
handleVote(-1)}
disabled={votePending}
- style={{ opacity: voteState === 1 ? 0.4 : 1 }}
>
-
+
@@ -102,11 +110,11 @@ function MagicCard({ articleId, articleUrl, source, timeAgo, sentiment, title, s
}
// Piccolo bottone riutilizzabile per mantenere coerente il footer azioni.
-function ActionButton({ label, tone, children, onClick, disabled, style }: { label: string; tone: string; children: ReactNode; onClick?: () => void; disabled?: boolean; style?: React.CSSProperties }) {
+function ActionButton({ label, tone, children, onClick, disabled, isActive, style }: { label: string; tone: string; children: ReactNode; onClick?: () => void; disabled?: boolean; isActive?: boolean; style?: React.CSSProperties }) {
return (