Spaces:
Sleeping
Sleeping
Download TruthScan AI_frontend/components/ArticleCard.tsx from Bjornpool/truthscan-ai-backend: direct link, hf CLI and curl.
- Browser
- Download file 7.72 kB
-
https://huggingface.co/spaces/Bjornpool/truthscan-ai-backend/resolve/a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan%20AI_frontend/components/ArticleCard.tsx
- Command line
-
hf download 'hf://spaces/Bjornpool/truthscan-ai-backend@a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan AI_frontend/components/ArticleCard.tsx'
-
curl -L -o ArticleCard.tsx https://huggingface.co/spaces/Bjornpool/truthscan-ai-backend/resolve/a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan%20AI_frontend/components/ArticleCard.tsx
7.72 kB
| /** | |
| * Komponent prezentujący pojedynczy artykuł informacyjny. | |
| * | |
| * Odpowiada za: | |
| * - wyświetlanie metadanych artykułu (tytuł, źródło, data, sentyment), | |
| * - obsługę akcji użytkownika (zapis, usunięcie, eksport do PDF), | |
| * - integrację z hookami pomocniczymi (sentyment, favicona, treść artykułu), | |
| * - renderowanie w trybie pełnym lub kompaktowym. | |
| */ | |
| "use client"; | |
| import React, { useState } from "react"; | |
| import { Article } from "../lib/fetchNews"; | |
| import { useArticleContent } from "../app/hooks/useArticleContent"; | |
| import { useFavicon } from "../app/hooks/useFavicon"; | |
| import { useSentiment } from "../app/hooks/useSentiment"; | |
| import PDFModal from "./PDFModal"; | |
| import type { Lang } from "../lib/types"; | |
| interface ArticleCardProps { | |
| article: Article; | |
| language: Lang; | |
| locales: any; | |
| onDelete?: (title: string) => void; | |
| savedView?: boolean; | |
| variant?: "full" | "compact"; | |
| onSave?: (article: Article) => void; | |
| } | |
| export default function ArticleCard({ | |
| article, | |
| language, | |
| locales, | |
| onDelete, | |
| savedView = false, | |
| variant = "full", | |
| onSave, | |
| }: ArticleCardProps) { | |
| const { label: sentimentLabel, color: sentimentColor } = useSentiment(article.sentiment || "", language); | |
| const { faviconSrc, sourceInitial, hasFavicon, nextFavicon } = useFavicon(article); | |
| const { fetchFullContent } = useArticleContent(); | |
| const [isExporting, setIsExporting] = useState(false); | |
| const [isLoadingContent, setIsLoadingContent] = useState(false); | |
| const [fullContent, setFullContent] = useState<string>(""); | |
| const [showPDF, setShowPDF] = useState(false); | |
| const isCompact = variant === "compact"; | |
| const t = locales[language] ?? locales.pl; | |
| const T = (pl: string, no: string, en: string) => | |
| language === "pl" ? pl : language === "no" ? no : en; | |
| const handleExportPDF = async () => { | |
| if (isExporting) return; | |
| setIsExporting(true); | |
| try { | |
| let contentToExport = article.summary || article.description || ""; | |
| if (article.link && article.link.startsWith('http') && !fullContent) { | |
| setIsLoadingContent(true); | |
| try { | |
| const fetchedContent = await fetchFullContent(article.link); | |
| setFullContent(fetchedContent); | |
| } catch (error) { | |
| console.log("ℹ️ Używam dostępnej treści"); | |
| } finally { | |
| setIsLoadingContent(false); | |
| } | |
| } | |
| setShowPDF(true); | |
| } catch (error) { | |
| console.error("❌ Błąd przygotowania PDF:", error); | |
| alert(T("Błąd podczas przygotowywania PDF", "Feil ved forberedelse av PDF", "Error preparing PDF")); | |
| } finally { | |
| setIsExporting(false); | |
| } | |
| }; | |
| const handlePDFClose = () => setShowPDF(false); | |
| const handlePDFExportStart = () => console.log("🟡 Eksport PDF rozpoczęty"); | |
| const handlePDFExportEnd = () => { | |
| console.log("🟢 Eksport PDF zakończony"); | |
| handlePDFClose(); | |
| }; | |
| const renderHeader = () => ( | |
| <div className="flex items-start gap-3 mb-2"> | |
| {hasFavicon ? ( | |
| <img | |
| src={faviconSrc} | |
| alt={`${article.source} favicon`} | |
| width={24} | |
| height={24} | |
| className="w-6 h-6 rounded-sm mt-1 ring-1 ring-black/10 dark:ring-white/10 bg-white object-contain" | |
| referrerPolicy="no-referrer" | |
| onError={nextFavicon} | |
| /> | |
| ) : ( | |
| <div className="w-6 h-6 rounded-sm mt-1 bg-gray-200 dark:bg-gray-700 grid place-items-center text-xs font-semibold"> | |
| {sourceInitial} | |
| </div> | |
| )} | |
| <h2 className={[ | |
| "font-bold text-blue-600 dark:text-blue-400", | |
| isCompact ? "text-base leading-snug" : "text-lg" | |
| ].join(" ")}> | |
| {article.title} | |
| </h2> | |
| </div> | |
| ); | |
| const renderDescription = () => ( | |
| <p className={[ | |
| "text-gray-700 dark:text-gray-300", | |
| isCompact ? "text-sm line-clamp-3 mb-2" : "mb-3" | |
| ].join(" ")}> | |
| {article.summary || article.description || | |
| T("Brak opisu", "Ingen beskrivelse", "No description")} | |
| </p> | |
| ); | |
| const renderMetadata = () => ( | |
| <div className={[ | |
| "text-gray-500 dark:text-gray-400", | |
| isCompact ? "text-xs space-y-0.5 mb-2" : "text-sm space-y-1 mb-3" | |
| ].join(" ")}> | |
| <p> | |
| 🕒 {T("Data", "Dato", "Date")}:{" "} | |
| <time suppressHydrationWarning> | |
| {article.published || T("Brak daty", "Ingen dato", "No date")} | |
| </time> | |
| </p> | |
| <p>📰 {T("Źródło", "Kilde", "Source")}: {article.source || "-"}</p> | |
| <p> | |
| 💬 {T("Sentyment", "Sentimentanalyse", "Sentiment")}:{" "} | |
| <span className={sentimentColor}>{sentimentLabel}</span> | |
| </p> | |
| <p> | |
| ❓ {T("Prawdopodobieństwo Fake News", "Sannsynlighet for falske nyheter", "Fake News Probability")}:{" "} | |
| {typeof article.fake_probability === "number" | |
| ? `${article.fake_probability.toFixed(2)}%` | |
| : T("Brak danych", "Ingen data", "No data")} | |
| </p> | |
| </div> | |
| ); | |
| const renderActions = () => ( | |
| <div className={[ | |
| "flex items-center gap-6 pt-2 border-t border-gray-300 dark:border-gray-700", | |
| isCompact ? "mt-2" : "mt-3" | |
| ].join(" ")}> | |
| {article.link && ( | |
| <a | |
| href={article.link.startsWith("http") ? article.link : `https://${article.link}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-500 hover:text-blue-400 font-medium transition" | |
| > | |
| 🔗 {T("Pokaż artykuł", "Vis artikkel", "View article")} | |
| </a> | |
| )} | |
| {savedView && ( | |
| <button | |
| onClick={handleExportPDF} | |
| disabled={isExporting || isLoadingContent} | |
| className={`text-purple-600 hover:text-purple-500 font-medium transition ${ | |
| (isExporting || isLoadingContent) ? 'opacity-50 cursor-not-allowed' : '' | |
| }`} | |
| > | |
| {isLoadingContent ? '⏳' : isExporting ? '📄' : '📄'} | |
| {isLoadingContent | |
| ? T(" Pobieranie...", " Laster inn...", " Loading...") | |
| : T(" Eksportuj PDF", " Eksporter til PDF", " Export PDF") | |
| } | |
| </button> | |
| )} | |
| {savedView ? ( | |
| <button | |
| onClick={() => { | |
| if (confirm(T( | |
| "Czy na pewno chcesz usunąć ten artykuł?", | |
| "Er du sikker på at du vil slette denne artikkelen?", | |
| "Are you sure you want to delete this article?" | |
| ))) { | |
| onDelete?.(article.title); | |
| } | |
| }} | |
| className="text-red-500 hover:text-red-400 font-medium transition" | |
| > | |
| 🗑️ {T("Usuń", "Slett", "Delete")} | |
| </button> | |
| ) : onSave ? ( | |
| <button | |
| onClick={() => onSave(article)} | |
| className="text-green-600 hover:text-green-500 font-medium transition" | |
| > | |
| 💾 {T("Zapisz", "Lagre", "Save")} | |
| </button> | |
| ) : null} | |
| </div> | |
| ); | |
| return ( | |
| <> | |
| <article className={[ | |
| "rounded-2xl border bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 transition-all duration-300", | |
| isCompact ? "p-4 shadow-sm hover:shadow-md" : "p-5 shadow-md hover:shadow-lg hover:-translate-y-0.5" | |
| ].join(" ")}> | |
| {renderHeader()} | |
| {renderDescription()} | |
| {renderMetadata()} | |
| {renderActions()} | |
| </article> | |
| <PDFModal | |
| article={article} | |
| language={language} | |
| sentimentLabel={sentimentLabel} | |
| sentimentColor={sentimentColor} | |
| fullContent={fullContent} | |
| isLoadingContent={isLoadingContent} | |
| showPDF={showPDF} | |
| onClose={handlePDFClose} | |
| onExportStart={handlePDFExportStart} | |
| onExportEnd={handlePDFExportEnd} | |
| /> | |
| </> | |
| ); | |
| } |