diff --git a/.gitignore b/.gitignore index bd0fc71..e90d1fe 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ __pycache__/ # OS .DS_Store Thumbs.db +web/client/package-lock.json diff --git a/web/client/src/Home.tsx b/web/client/src/Home.tsx index fe46196..6b82def 100644 --- a/web/client/src/Home.tsx +++ b/web/client/src/Home.tsx @@ -1,303 +1,856 @@ -import { useEffect, useState } from "react"; +// COMANDO PARA INSTALAR DEPENDÊNCIAS: +// npm install lucide-react dompurify +// npm install -D @types/dompurify + +import { useEffect, useMemo, useState } from "react"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "./components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "./components/ui/tabs"; -import { Pagination, PaginationContent, PaginationItem } from "./components/ui/pagination"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationPrevious, + PaginationNext, + PaginationEllipsis +} from "./components/ui/pagination"; import { Badge } from "./components/ui/badge"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "./components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogTrigger, + DialogDescription +} from "./components/ui/dialog"; import { Skeleton } from "./components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { Search, X, Newspaper, CalendarDays, Tags, ExternalLink, Star, Frown, Clock, Link as LinkIcon, Check } from "lucide-react"; + +// Biblioteca de Segurança +import DOMPurify from 'dompurify'; -const API_URL = "http://localhost:8000"; +// Importações de assets +import veja from './public/assets/veja.jpeg'; +import metropoles from './public/assets/metropoles.jpeg' +import r7 from './public/assets/r7.png' +import uol from './public/assets/uol.png' +import maisgoias from './public/assets/maisgoias.jpeg' +import aliadosbrasil from './public/assets/aliadosbrasil.jpeg' +import ig from './public/assets/IG.png' +import folha from './public/assets/folha.jpeg' + +const API_URL = "http://localhost:8000" + +const portalIcons: Record = { + metropoles_com: metropoles, + veja_abril_com_br: veja, + r7_com: r7, + uol_com_br: uol, + maisgoias_com_br: maisgoias, + aliadosbrasiloficial_com_br: aliadosbrasil, + ig_com_br: ig, + folha_uol_com_br: folha, +}; -// Função utilitária para formatar o nome do portal function formatarNomePortal(portal: string) { - const nome = portal.replace(/_/g, ".").toLowerCase(); - return nome.charAt(0).toUpperCase() + nome.slice(1); + const nome = portal.replace(/_/g, ".").toLowerCase(); + return nome.charAt(0).toUpperCase() + nome.slice(1); +} +function limparLixoVisual(html: string) { + if (!html) return ""; + let linhas = html.split('\n'); + let modoExclusao = false; + + const termosBanidos = [ + "FONTE/CRÉDITOS", "FONTE/CREDITOS", + "CRÉDITOS (IMAGEM DE CAPA)", "CRÉDITOS:", "CREDITOS:", "FONTE:", + ,"PUBLICIDADE","MAIS LIDAS" // Tags soltas + ]; + + // Filtra as linhas + linhas = linhas.filter(linha => { + const conteudo = linha.trim(); + const conteudoUpper = conteudo.toUpperCase(); + + // 1. Verifica se a linha começa o bloco "LEIA TAMBÉM" + if (conteudoUpper.includes("LEIA TAMBÉM") || conteudoUpper.includes("LEIA TAMBEM")) { + modoExclusao = true; + return false; + } + + // 2. Se estivermos no modo de exclusão (logo após um LEIA TAMBÉM) + if (modoExclusao) { + if (conteudo.length < 150 && conteudo.length > 0) { + return false; + } else if (conteudo.length >= 150) { + modoExclusao = false; + } + } + + return true; + }); + + return linhas.join('\n'); +} +function calcularTempoLeitura(texto: string | undefined) { + if (!texto) return "1 min"; + const palavras = texto.split(/\s+/).length; + const minutos = Math.ceil(palavras / 200); + return `${minutos} min`; } const Home = () => { - const [portais, setPortais] = useState([]); - const [loading, setLoading] = useState(true); - const [erro, setErro] = useState(""); - const [portalSelecionado, setPortalSelecionado] = useState( - null, - ); - const [busca, setBusca] = useState(""); - const [noticias, setNoticias] = useState([]); - const [carregandoNoticias, setCarregandoNoticias] = useState(false); - const [erroNoticias, setErroNoticias] = useState(""); - const [pagina, setPagina] = useState(1); - const noticiasPorPagina = 10; - const [noticiaSelecionada, setNoticiaSelecionada] = useState(null); - const [dialogAberto, setDialogAberto] = useState(false); - const [noticiasPorPortal, setNoticiasPorPortal] = useState>({}); - - useEffect(() => { - fetch(`${API_URL}/portais`) - .then((res) => res.json()) - .then(setPortais) - .catch(() => setErro("Erro ao buscar portais")) - .finally(() => setLoading(false)); - }, []); - - useEffect(() => { - setPagina(1); // Sempre volta para a primeira página ao trocar portal ou busca - }, [portalSelecionado, busca]); - - useEffect(() => { - if (!portalSelecionado) return; - setCarregandoNoticias(true); - setErroNoticias(""); - const url = busca.trim() - ? `${API_URL}/noticias/${portalSelecionado}/search?q=${encodeURIComponent(busca)}` - : `${API_URL}/noticias/${portalSelecionado}`; - fetch(url) - .then((res) => res.json()) - .then(setNoticias) - .catch(() => setErroNoticias("Erro ao buscar notícias")) - .finally(() => setCarregandoNoticias(false)); - }, [portalSelecionado, busca]); - - // Paginação no frontend - const totalPaginas = Math.ceil(noticias.length / noticiasPorPagina); - const inicio = (pagina - 1) * noticiasPorPagina; - const fim = inicio + noticiasPorPagina; - const noticiasPaginadas = noticias.slice(inicio, fim); - - useEffect(() => { - // Buscar contagem de notícias para cada portal - const buscarContagemNoticias = async () => { - const contagem: Record = {}; - for (const portal of portais) { - try { - const response = await fetch(`${API_URL}/noticias/${portal}`); - const noticias = await response.json(); - contagem[portal] = noticias.length; - } catch (error) { - contagem[portal] = 0; - } - } - setNoticiasPorPortal(contagem); - }; - - if (portais.length > 0) { - buscarContagemNoticias(); - } - }, [portais]); - - if (loading) return ( -
-

- Selecione um portal de notícias -

-
- {/* Skeleton das Tabs */} -
- {Array.from({ length: 4 }, (_, idx) => ( -
- - -
- ))} -
- {/* Skeleton do conteúdo */} -
- - -
- {Array.from({ length: 3 }, (_, idx) => ( - - - - - - - - - - - - - ))} -
-
-
-
- ); - if (erro) return
{erro}
; - - return ( -
-

- Selecione um portal de notícias -

- {portais.length > 0 && ( - - - {portais.map((portal) => ( - - - {noticiasPorPortal[portal] || 0} - - {formatarNomePortal(portal)} - - ))} - - {portais.map((portal) => ( - -
- Portal selecionado: {formatarNomePortal(portal)} -
-
-
- setBusca(e.target.value)} - /> -
- {carregandoNoticias ? ( -
- {Array.from({ length: 5 }, (_, idx) => ( - - - - - - - - - - - - - ))} -
- ) : erroNoticias ? ( -
{erroNoticias}
- ) : noticiasPaginadas.length === 0 ? ( -
Nenhuma notícia encontrada.
- ) : ( -
- {noticiasPaginadas.map((noticia, idx) => ( - { - setDialogAberto(open); - if (!open) setNoticiaSelecionada(null); - }}> - - { - setNoticiaSelecionada(noticia); - setDialogAberto(true); - }}> - - {noticia.title} - - - {noticia.description && ( -
{noticia.description}
- )} -
- {noticia.link && ( - - Ler mais - - )} -
-
- - - {noticia.title} - -
- {noticia.description && ( -
-

Descrição

-

{noticia.description}

-
- )} - {noticia.body && ( -
-

Conteúdo

-
-
- )} - {noticia.tags && noticia.tags.length > 0 && ( -
-

Tags

-
- {noticia.tags.map((tag: string, tagIdx: number) => ( - {tag} - ))} -
-
- )} - {noticia.date && ( -
-

Data

-

{new Date(noticia.date).toLocaleDateString('pt-BR')}

-
- )} -
- {noticia.url && ( - - )} - -
- ))} -
- )} - {totalPaginas > 1 && ( -
-

- Página {pagina} de{" "} - {totalPaginas} -

- - - - - - - - - - -
- )} -
-
- ))} -
- )} -
- ); + const [portais, setPortais] = useState([]); + const [loading, setLoading] = useState(true); + const [erro, setErro] = useState(""); + const [portalSelecionado, setPortalSelecionado] = useState(null); + const [noticias, setNoticias] = useState([]); + const [todasAsNoticiasDoPortal, setTodasAsNoticiasDoPortal] = useState([]); + const [carregandoNoticias, setCarregandoNoticias] = useState(false); + const [erroNoticias, setErroNoticias] = useState(""); + const [noticiaSelecionada, setNoticiaSelecionada] = useState(null); + const [dialogAberto, setDialogAberto] = useState(false); + const [noticiasPorPortal, setNoticiasPorPortal] = useState>({}); + const [tamanhoFonte, setTamanhoFonte] = useState("text-base"); + const [copiadoId, setCopiadoId] = useState(null); // Para feedback visual + + // Estados para a seção de PORTAIS + const [buscaPortal, setBuscaPortal] = useState(""); + const [paginaPortal, setPaginaPortal] = useState(1); + const [topTagsPortal, setTopTagsPortal] = useState([]); + + // Estados para a seção de FAVORITOS + const [buscaFavoritos, setBuscaFavoritos] = useState(""); + const [paginaFavoritos, setPaginaFavoritos] = useState(1); + const [topTagsFavoritos, setTopTagsFavoritos] = useState([]); + + const noticiasPorPagina = 10; + + // --- LÓGICA DE FAVORITOS (Início) --- + const [favoritos, setFavoritos] = useState(() => { + try { + const saved = localStorage.getItem("favoritos"); + return saved ? JSON.parse(saved) : []; + } catch (error) { + console.error("Erro ao ler favoritos do localStorage", error); + return []; + } + }); + + useEffect(() => { + localStorage.setItem("favoritos", JSON.stringify(favoritos)); + }, [favoritos]); + + const isNoticiaFavorita = (noticia: any) => { + return favoritos.some(fav => fav.url === noticia.url); + }; + + const handleToggleFavorito = (noticia: any) => { + if (isNoticiaFavorita(noticia)) { + setFavoritos(prev => prev.filter(fav => fav.url !== noticia.url)); + } else { + setFavoritos(prev => [noticia, ...prev]); + } + }; + + const handleCopiarLink = (url: string, id: string) => { + navigator.clipboard.writeText(url); + setCopiadoId(id); + setTimeout(() => setCopiadoId(null), 2000); + } + + // --- LÓGICA DE FAVORITOS (Fim) --- + + + useEffect(() => { + fetch(`${API_URL}/portais`) + .then((res) => res.json()) + .then(setPortais) + .catch(() => setErro("Erro ao buscar portais")) + .finally(() => setLoading(false)); + }, []); + + // Reseta paginação do PORTAL quando a busca ou portal muda + useEffect(() => { + setPaginaPortal(1); + }, [portalSelecionado, buscaPortal]); + + // Reseta paginação dos FAVORITOS quando a busca de favoritos muda + useEffect(() => { + setPaginaFavoritos(1); + }, [buscaFavoritos]); + + // Busca notícias da API (para portais) + useEffect(() => { + if (!portalSelecionado || portalSelecionado === 'favoritos') { + setNoticias([]); + setTodasAsNoticiasDoPortal([]); + return; + } + + setCarregandoNoticias(true); + setErroNoticias(""); + + if (!buscaPortal.trim()) { + fetch(`${API_URL}/noticias/${portalSelecionado}`) + .then((res) => res.json()) + .then((data) => { + setNoticias(data); + setTodasAsNoticiasDoPortal(data); + }) + .catch(() => setErroNoticias("Erro ao buscar notícias")) + .finally(() => setCarregandoNoticias(false)); + } else { + const url = `${API_URL}/noticias/${portalSelecionado}/search?q=${encodeURIComponent(buscaPortal)}`; + fetch(url) + .then((res) => res.json()) + .then(setNoticias) + .catch(() => setErroNoticias("Erro ao buscar notícias")) + .finally(() => setCarregandoNoticias(false)); + } + }, [portalSelecionado, buscaPortal]); + + // Calcula as Top Tags do PORTAL + useEffect(() => { + if (todasAsNoticiasDoPortal.length > 0) { + const allTags = todasAsNoticiasDoPortal.flatMap(noticia => noticia.tags || []); + const tagCounts = allTags.reduce((acc, tag) => { + acc[tag] = (acc[tag] || 0) + 1; + return acc; + }, {} as Record); + const sortedTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]); + setTopTagsPortal(sortedTags.slice(0, 30)); + } else { + setTopTagsPortal([]); + } + }, [todasAsNoticiasDoPortal]); + + // Calcula as Top Tags dos FAVORITOS + useEffect(() => { + if (favoritos.length > 0) { + const allTags = favoritos.flatMap(noticia => noticia.tags || []); + const tagCounts = allTags.reduce((acc, tag) => { + acc[tag] = (acc[tag] || 0) + 1; + return acc; + }, {} as Record); + const sortedTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]); + setTopTagsFavoritos(sortedTags.slice(0, 30)); + } else { + setTopTagsFavoritos([]); + } + }, [favoritos]); + + + // Paginação e dados para PORTAIS + const totalPaginasPortal = Math.ceil(noticias.length / noticiasPorPagina); + const inicioPortal = (paginaPortal - 1) * noticiasPorPagina; + const fimPortal = inicioPortal + noticiasPorPagina; + const noticiasPaginadasPortal = noticias.slice(inicioPortal, fimPortal); + + // Filtro e Paginação para FAVORITOS + const favoritosFiltrados = useMemo(() => { + if (!buscaFavoritos.trim()) { + return favoritos; + } + const lowerBusca = buscaFavoritos.toLowerCase(); + return favoritos.filter(noticia => + noticia.title?.toLowerCase().includes(lowerBusca) || + noticia.description?.toLowerCase().includes(lowerBusca) || + noticia.tags?.some((tag: string) => tag.toLowerCase().includes(lowerBusca)) + ); + }, [favoritos, buscaFavoritos]); + + const totalPaginasFavoritos = Math.ceil(favoritosFiltrados.length / noticiasPorPagina); + const inicioFavoritos = (paginaFavoritos - 1) * noticiasPorPagina; + const fimFavoritos = inicioFavoritos + noticiasPorPagina; + const noticiasPaginadasFavoritos = favoritosFiltrados.slice(inicioFavoritos, fimFavoritos); + + + // Contagem de notícias (igual) + useEffect(() => { + const buscarContagemNoticias = async () => { + const contagem: Record = {}; + for (const portal of portais) { + try { + const response = await fetch(`${API_URL}/noticias/${portal}`); + const noticiasPortal = await response.json(); + contagem[portal] = noticiasPortal.length; + } catch (error) { + contagem[portal] = 0; + } + } + setNoticiasPorPortal(contagem); + }; + + if (portais.length > 0) { + buscarContagemNoticias(); + } + }, [portais]); + + // Handlers de clique + const handleTagClickPortal = (tag: string) => { + setBuscaPortal(buscaPortal === tag ? "" : tag); + }; + + const handleTagClickFavoritos = (tag: string) => { + setBuscaFavoritos(buscaFavoritos === tag ? "" : tag); + }; + + const handlePortalChange = (novoPortal: string) => { + setPortalSelecionado(novoPortal); + setBuscaPortal(""); // Limpa ambas as buscas + setBuscaFavoritos(""); + }; + + // Agora é uma função que aceita parâmetros + const getPaginationRange = (totalPages: number, currentPage: number) => { + const siblingCount = 1; + const totalPageNumbers = siblingCount + 5; + + if (totalPageNumbers >= totalPages) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + const leftSiblingIndex = Math.max(currentPage - siblingCount, 1); + const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages); + const shouldShowLeftDots = leftSiblingIndex > 2; + const shouldShowRightDots = rightSiblingIndex < totalPages - 2; + const firstPageIndex = 1; + const lastPageIndex = totalPages; + + if (!shouldShowLeftDots && shouldShowRightDots) { + let leftItemCount = 3 + 2 * siblingCount; + let leftRange = Array.from({ length: leftItemCount }, (_, i) => i + 1); + return [...leftRange, "...", totalPages]; + } + if (shouldShowLeftDots && !shouldShowRightDots) { + let rightItemCount = 3 + 2 * siblingCount; + let rightRange = Array.from({ length: rightItemCount }, (_, i) => totalPages - i).reverse(); + return [firstPageIndex, "...", ...rightRange]; + } + if (shouldShowLeftDots && shouldShowRightDots) { + let middleRange = Array.from({ length: rightSiblingIndex - leftSiblingIndex + 1 }, (_, i) => leftSiblingIndex + i); + return [firstPageIndex, "...", ...middleRange, "...", lastPageIndex]; + } + return []; + }; + + // Geradores de números de página + const pageNumbersPortal = getPaginationRange(totalPaginasPortal, paginaPortal); + const pageNumbersFavoritos = getPaginationRange(totalPaginasFavoritos, paginaFavoritos); + + const RenderNoticiasGrid = ({ noticiasParaRenderizar }: { noticiasParaRenderizar: any[] }) => { + return ( +
+ {noticiasParaRenderizar.map((noticia, idx) => ( + { + setDialogAberto(open); + if (!open) setNoticiaSelecionada(null); + }}> + + { + setNoticiaSelecionada(noticia); + setDialogAberto(true); + }} + > + +
+ + {noticia.title} + + {noticia.description && ( + +

{noticia.description}

+
+ )} +
+ +
+ {noticia.date ? ( + + {new Date(noticia.date).toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })} + + ) :
} + +
+ + {calcularTempoLeitura(noticia.body || noticia.description)} +
+
+ + {noticia.tags && noticia.tags.length > 0 && ( +
+ {noticia.tags.slice(0, 2).map((tag: string, tagIdx: number) => ( + + {tag} + + ))} + {noticia.tags.length > 2 && ( + + +{noticia.tags.length - 2} + + )} +
+ )} + + + + + + +
+
+ + + {calcularTempoLeitura(noticia.body || noticia.description)} de leitura + +
+
+ Tamanho da fonte: + + + +
+
+ + {noticia.title} + +
+ {noticia.date && ( +
+ + {new Date(noticia.date).toLocaleDateString('pt-BR', { dateStyle: 'long' })} +
+ )} + {noticia.tags && noticia.tags.length > 0 && ( +
+ + Tags: + {noticia.tags.join(', ')} +
+ )} +
+
+
+
+
+ {noticia.description && ( +

+ {noticia.description} +

+ )} + {noticia.body && ( +
+
+
+ )} +
+
+ {noticia.url && ( + + {/* Botão Copiar Link */} + + + + + )} + + +
+ ))} +
+ ); + }; + + // ESTADO DE LOADING (igual) + if (loading) return ( +
+

+ Selecione um portal de notícias +

+ {/* ... (skeleton) ... */} +
+
+ {Array.from({ length: 4 }, (_, idx) => ( +
+ + + +
+ ))} +
+
+ + +
+ {Array.from({ length: 3 }, (_, idx) => ( + + + + + + + + + + + + + ))} +
+
+
+
+ ); + + // ESTADO DE ERRO (igual) + if (erro) return
{erro}
; + + // ESTADO PRINCIPAL (CARREGADO) + return ( +
+

+ Selecione um portal de notícias +

+ {portais.length > 0 && ( + + + + + Favoritos + + {favoritos.length} + + + {portais.map((portal) => ( + + {portalIcons[portal] && ( + {`${formatarNomePortal(portal)} { (e.target as HTMLImageElement).style.display = 'none'; }} + /> + )} + {formatarNomePortal(portal)} + + {noticiasPorPortal[portal] ?? } + + + ))} + + + {/* Estado de Boas-Vindas (igual) */} + {!portalSelecionado && ( +
+ +

+ Bem-vindo ao seu Agregador de Notícias +

+

+ Selecione um portal ou seus favoritos acima. +

+
+ )} + + {/* Conteúdo dos Portais */} + {portais.map((portal) => ( + +
+ Portal selecionado: {formatarNomePortal(portal)} +
+
+ + +
+
+ + setBuscaPortal(e.target.value)} + className="pl-10 pr-10" + /> + {buscaPortal.length > 0 && ( + + )} +
+ {carregandoNoticias ? ( +
+ {Array.from({ length: 6 }, (_, idx) => ( + + + + + + + + + + + + + ))} +
+ ) : erroNoticias ? ( +
{erroNoticias}
+ ) : noticiasPaginadasPortal.length === 0 ? ( +
Nenhuma notícia encontrada{buscaPortal.trim() && ` para "${buscaPortal}"`}.
+ ) : ( + + )} + + {/* Paginação do PORTAL */} + {totalPaginasPortal > 1 && ( +
+

+ Página {paginaPortal} de{" "} + {totalPaginasPortal} +

+ + + + { e.preventDefault(); setPaginaPortal((p) => Math.max(1, p - 1)); }} + className={paginaPortal === 1 ? "pointer-events-none opacity-50" : undefined} + /> + + {pageNumbersPortal.map((page, idx) => ( + + {typeof page === 'string' ? ( + + ) : ( + { e.preventDefault(); setPaginaPortal(page); }} + isActive={paginaPortal === page} + > + {page} + + )} + + ))} + + { e.preventDefault(); setPaginaPortal((p) => Math.min(totalPaginasPortal, p + 1)); }} + className={paginaPortal === totalPaginasPortal ? "pointer-events-none opacity-50" : undefined} + /> + + + +
+ )} +
+
+
+ ))} + + {/* --- CONTEÚDO DA ABA FAVORITOS --- */} + +
+ Seção selecionada: Favoritos +
+ + {favoritos.length === 0 ? ( +
+ +

+ Nenhum favorito salvo +

+

+ Clique na estrela ( ) em qualquer notícia para salvá-la aqui. +

+
+ ) : ( + // Layout de 2 colunas para favoritos +
+ + +
+
+ + setBuscaFavoritos(e.target.value)} + className="pl-10 pr-10" + /> + {buscaFavoritos.length > 0 && ( + + )} +
+ + {favoritosFiltrados.length === 0 ? ( +
Nenhum favorito encontrado{buscaFavoritos.trim() && ` para "${buscaFavoritos}"`}.
+ ) : ( + + )} + + {/* Paginação dos FAVORITOS */} + {totalPaginasFavoritos > 1 && ( +
+

+ Página {paginaFavoritos} de{" "} + {totalPaginasFavoritos} +

+ + + + { e.preventDefault(); setPaginaFavoritos((p) => Math.max(1, p - 1)); }} + className={paginaFavoritos === 1 ? "pointer-events-none opacity-50" : undefined} + /> + + {pageNumbersFavoritos.map((page, idx) => ( + + {typeof page === 'string' ? ( + + ) : ( + { e.preventDefault(); setPaginaFavoritos(page); }} + isActive={paginaFavoritos === page} + > + {page} + + )} + + ))} + + { e.preventDefault(); setPaginaFavoritos((p) => Math.min(totalPaginasFavoritos, p + 1)); }} + className={paginaFavoritos === totalPaginasFavoritos ? "pointer-events-none opacity-50" : undefined} + /> + + + +
+ )} +
+
+ )} +
+
+ )} +
+ ); }; -export default Home; +export default Home; \ No newline at end of file diff --git a/web/client/src/components/ui/card.tsx b/web/client/src/components/ui/card.tsx index d05bbc6..5386961 100644 --- a/web/client/src/components/ui/card.tsx +++ b/web/client/src/components/ui/card.tsx @@ -1,5 +1,4 @@ import * as React from "react" - import { cn } from "@/lib/utils" function Card({ className, ...props }: React.ComponentProps<"div">) { @@ -89,4 +88,4 @@ export { CardAction, CardDescription, CardContent, -} +} \ No newline at end of file diff --git a/web/client/src/components/ui/dialog.tsx b/web/client/src/components/ui/dialog.tsx index 6cb123b..3f27c46 100644 --- a/web/client/src/components/ui/dialog.tsx +++ b/web/client/src/components/ui/dialog.tsx @@ -58,7 +58,10 @@ function DialogContent({ Close @@ -82,7 +89,11 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
) @@ -93,7 +104,8 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
) @@ -121,7 +136,10 @@ function DialogDescription({ return ( ) @@ -138,4 +156,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +} \ No newline at end of file diff --git a/web/client/src/public/assets/IG.png b/web/client/src/public/assets/IG.png new file mode 100644 index 0000000..98b0a16 Binary files /dev/null and b/web/client/src/public/assets/IG.png differ diff --git a/web/client/src/public/assets/aliadosbrasil.jpeg b/web/client/src/public/assets/aliadosbrasil.jpeg new file mode 100644 index 0000000..1ece7e1 Binary files /dev/null and b/web/client/src/public/assets/aliadosbrasil.jpeg differ diff --git a/web/client/src/public/assets/folha.jpeg b/web/client/src/public/assets/folha.jpeg new file mode 100644 index 0000000..711d74d Binary files /dev/null and b/web/client/src/public/assets/folha.jpeg differ diff --git a/web/client/src/public/assets/maisgoias.jpeg b/web/client/src/public/assets/maisgoias.jpeg new file mode 100644 index 0000000..7aa438e Binary files /dev/null and b/web/client/src/public/assets/maisgoias.jpeg differ diff --git a/web/client/src/public/assets/metropoles.jpeg b/web/client/src/public/assets/metropoles.jpeg new file mode 100644 index 0000000..24d5dba Binary files /dev/null and b/web/client/src/public/assets/metropoles.jpeg differ diff --git a/web/client/src/public/assets/r7.png b/web/client/src/public/assets/r7.png new file mode 100644 index 0000000..edf1bef Binary files /dev/null and b/web/client/src/public/assets/r7.png differ diff --git a/web/client/src/public/assets/uol.png b/web/client/src/public/assets/uol.png new file mode 100644 index 0000000..4071c1a Binary files /dev/null and b/web/client/src/public/assets/uol.png differ diff --git a/web/client/src/public/assets/veja.jpeg b/web/client/src/public/assets/veja.jpeg new file mode 100644 index 0000000..0ff22d2 Binary files /dev/null and b/web/client/src/public/assets/veja.jpeg differ diff --git a/web/server/main.py b/web/server/main.py index c255fee..1357ae3 100644 --- a/web/server/main.py +++ b/web/server/main.py @@ -45,7 +45,6 @@ def ping(): @app.get("/portais", response_model=List[str]) def listar_portais(): - # Lista os prefixos (portais) diretamente do MinIO portais = set() try: objetos = minio_client.list_objects(MINIO_BUCKET, recursive=False) @@ -54,18 +53,32 @@ def listar_portais(): nome_portal = obj.object_name.rstrip('/') portais.add(nome_portal) else: - # Caso não venha como diretório, extrai o prefixo do nome do - # objeto partes = obj.object_name.split('/') if len(partes) > 1: portais.add(partes[0]) except Exception as e: raise HTTPException( status_code=500, - detail=f"Erro ao acessar o MinIO: { - str(e)}") + detail=f"Erro ao acessar o MinIO: {str(e)}") return sorted(list(portais)) +# --- FUNÇÃO AUXILIAR PARA TRATAR DATA --- +def processar_data_noticia(noticia): + data_str = ( + noticia.get('date') or + noticia.get('publishedAt') or + noticia.get('createdAt') or + noticia.get('scraped_at') + ) + if data_str: + noticia['date'] = data_str + try: + data_limpa = data_str[:19] + return datetime.fromisoformat(data_limpa) + except Exception: + pass + + return datetime.min @app.get("/noticias/{portal}") def listar_noticias( @@ -84,33 +97,21 @@ def listar_noticias( MINIO_BUCKET, obj.object_name) conteudo = response.read() noticia = json.loads(conteudo.decode('utf-8')) - # Padronizar campo de data - data_str = noticia.get('date') or noticia.get( - 'publishedAt') or noticia.get('createdAt') - if data_str: - try: - data = datetime.fromisoformat(data_str[:10]) - noticia['_data'] = data - except Exception: - noticia['_data'] = datetime.min - else: - noticia['_data'] = datetime.min + noticia['_data_ordenacao'] = processar_data_noticia(noticia) + noticias.append(noticia) except Exception: continue except Exception as e: raise HTTPException( status_code=500, - detail=f"Erro ao acessar o MinIO: { - str(e)}") + detail=f"Erro ao acessar o MinIO: {str(e)}") # Ordenação reverse = order == 'desc' - noticias.sort(key=lambda n: n['_data'], reverse=reverse) - - # Remover campo auxiliar + noticias.sort(key=lambda n: n['_data_ordenacao'], reverse=reverse) for n in noticias: - n.pop('_data', None) + n.pop('_data_ordenacao', None) return noticias @@ -134,40 +135,32 @@ def buscar_noticias( MINIO_BUCKET, obj.object_name) conteudo = response.read() noticia = json.loads(conteudo.decode('utf-8')) - # Busca em título, descrição, body e tags + campos = [ str(noticia.get('title', '')), str(noticia.get('description', '')), str(noticia.get('body', '')), ' '.join(noticia.get('tags', []) or []) ] + if any(termo in campo.lower() for campo in campos): - # Padronizar campo de data - data_str = noticia.get('date') or noticia.get( - 'publishedAt') or noticia.get('createdAt') - if data_str: - try: - data = datetime.fromisoformat(data_str[:10]) - noticia['_data'] = data - except Exception: - noticia['_data'] = datetime.min - else: - noticia['_data'] = datetime.min + # Usa a mesma função auxiliar + noticia['_data_ordenacao'] = processar_data_noticia(noticia) resultados.append(noticia) + except Exception: continue except Exception as e: raise HTTPException( status_code=500, - detail=f"Erro ao acessar o MinIO: { - str(e)}") + detail=f"Erro ao acessar o MinIO: {str(e)}") # Ordenação reverse = order == 'desc' - resultados.sort(key=lambda n: n['_data'], reverse=reverse) + resultados.sort(key=lambda n: n['_data_ordenacao'], reverse=reverse) # Remover campo auxiliar for n in resultados: - n.pop('_data', None) + n.pop('_data_ordenacao', None) - return resultados + return resultados \ No newline at end of file