-
+
{candidateDetails && (
{candidateDetails.nome}
diff --git a/src/components/CandidatePage/ElectionsComponent.tsx b/src/components/CandidatePage/ElectionsComponent.tsx
index 79ee5bb..6c52f0e 100644
--- a/src/components/CandidatePage/ElectionsComponent.tsx
+++ b/src/components/CandidatePage/ElectionsComponent.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { DocumentTextIcon } from '@heroicons/react/24/outline';
import { type Election } from '../../api';
-import Tooltip from '../Tooltip';
+import Tooltip from '../../Components/Tooltip';
interface ElectionsComponentProps {
elections: Election[] | null;
diff --git a/src/components/CandidatePage/SocialMediaComponent.tsx b/src/components/CandidatePage/SocialMediaComponent.tsx
index 989b752..b9bc577 100644
--- a/src/components/CandidatePage/SocialMediaComponent.tsx
+++ b/src/components/CandidatePage/SocialMediaComponent.tsx
@@ -21,35 +21,36 @@ interface SocialMediaComponentProps {
}
// Helper function to get social media icons
-const getSocialMediaIcon = (rede: string): React.ReactElement => {
+const getSocialMediaIcon = (rede: string, isRecent: boolean = true): React.ReactElement => {
const iconClass = "h-5 w-5 mr-2";
+ const opacityClass = isRecent ? "" : "opacity-50";
switch (rede.toLowerCase()) {
case 'facebook':
- return
;
+ return
;
case 'instagram':
- return
;
+ return
;
case 'x/twitter':
case 'twitter':
- return
;
+ return
;
case 'tiktok':
- return
;
+ return
;
case 'youtube':
- return
;
+ return
;
case 'linkedin':
- return
;
+ return
;
case 'whatsapp':
- return
;
+ return
;
case 'threads':
- return
;
+ return
;
case 'telegram':
- return
;
+ return
;
case 'spotify':
- return
;
+ return
;
case 'kwai':
case 'outros':
default:
- return
;
+ return
;
}
};
@@ -57,6 +58,11 @@ const SocialMediaComponent: React.FC
= ({
redesSociais,
isLoading
}) => {
+ // Calculate the most recent year from all social media entries
+ const mostRecentYear = redesSociais && redesSociais.length > 0
+ ? Math.max(...redesSociais.map(rede => rede.ano))
+ : null;
+
return (
@@ -70,27 +76,41 @@ const SocialMediaComponent: React.FC = ({
) : redesSociais && redesSociais.length > 0 ? (
- {redesSociais.map((redeSocial: RedeSocial, index: number) => (
-
-
-
-
- {getSocialMediaIcon(redeSocial.rede)}
-
{redeSocial.rede}
-
({redeSocial.ano})
+ {redesSociais.map((redeSocial: RedeSocial, index: number) => {
+ const isRecent = redeSocial.ano === mostRecentYear;
+ return (
+
-
- ))}
+ );
+ })}
) : (
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
new file mode 100644
index 0000000..c38f6c6
--- /dev/null
+++ b/src/components/Card.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+interface CardProps {
+ children: React.ReactNode;
+ className?: string;
+ hasAnimation?: boolean;
+ disableCursor?: boolean;
+ height?: number;
+ width?: number;
+}
+
+const Card: React.FC
= ({ children, className = "", hasAnimation = false, disableCursor = false, height, width }) => {
+ const animationClasses = hasAnimation ? "hover:shadow-xl transform hover:scale-[1.01] transition-all duration-200 hover:ring-indigo-300" : "";
+ const cursorClasses = disableCursor ? "hover:cursor-default" : "";
+
+ const sizeStyles = {
+ ...(height && { height: `${height}rem` }),
+ ...(width && { width: `${width}rem` })
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Card;
diff --git a/src/components/DataStatsPage.tsx b/src/components/DataStatsPage.tsx
new file mode 100644
index 0000000..0dc566c
--- /dev/null
+++ b/src/components/DataStatsPage.tsx
@@ -0,0 +1,186 @@
+import React, { useState, useEffect } from 'react';
+import { openCandApi } from '../api';
+import type { OpenCandDataAvailabilityStats } from '../api/apiModels';
+import Card from '../Components/Card';
+
+const DataStatsPage: React.FC = () => {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchStats = async () => {
+ try {
+ setLoading(true);
+ const data = await openCandApi.getDataAvailabilityStats();
+ setStats(data);
+ } catch (err) {
+ setError('Erro ao carregar estatísticas de disponibilidade de dados');
+ console.error('Error fetching data availability stats:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchStats();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+
Carregando dados...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!stats) {
+ return (
+
+
Nenhum dado disponível
+
+ );
+ }
+
+ // Get all unique years from all data types
+ const allYears = new Set();
+ Object.values(stats).forEach((yearArray: number[]) => {
+ yearArray.forEach((year: number) => allYears.add(year));
+ });
+ const sortedYears = Array.from(allYears).sort((a, b) => b - a);
+
+ const dataTypes = [
+ { key: 'candidatos', label: 'Candidatos', icon: '👤' },
+ { key: 'bemCandidatos', label: 'Bens de Candidatos', icon: '💰' },
+ { key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
+ { key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
+ { key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
+ { key: 'fotosCandidatos', label: 'Fotos de Candidatos', icon: '📸' },
+ ];
+
+ return (
+
+
+ {/* Header */}
+
+
+ Disponibilidade de Dados
+
+
+ Visualize a disponibilidade dos dados por ano em nossa base
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+
📊
+
{dataTypes.length}
+
Tipos de Dados
+
+
+
+
+
📅
+
{sortedYears.length}
+
Anos Disponíveis
+
+
+
+
+
🗓️
+
+ {sortedYears.length > 0 ? `${Math.min(...sortedYears)} - ${Math.max(...sortedYears)}` : 'N/A'}
+
+
Período
+
+
+
+
+ {/* Data Availability Table */}
+
+
+
+ Matriz de Disponibilidade
+
+
+ ✅ Disponível • ❌ Não Disponível
+
+
+
+
+
+
+
+
+ Tipo de Dado
+ |
+ {sortedYears.map((year, index) => (
+
+ {year}
+ |
+ ))}
+
+
+
+ {dataTypes.map((dataType, rowIndex) => (
+
+
+
+ {dataType.icon}
+ {dataType.label}
+
+ |
+ {sortedYears.map((year, cellIndex) => {
+ const isAvailable = (stats[dataType.key as keyof OpenCandDataAvailabilityStats] as number[]).includes(year);
+ return (
+
+
+ {isAvailable ? '✅' : '❌'}
+
+ |
+ );
+ })}
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default DataStatsPage;
diff --git a/src/components/FeaturesSection.tsx b/src/components/FeaturesSection.tsx
index 451c2ae..a4c7bed 100644
--- a/src/components/FeaturesSection.tsx
+++ b/src/components/FeaturesSection.tsx
@@ -1,19 +1,20 @@
import React from 'react';
import { ArrowDownOnSquareStackIcon, BookOpenIcon, ChartBarIcon, DocumentTextIcon, LightBulbIcon } from '@heroicons/react/24/outline';
+import Card from '../Components/Card';
const FeatureCard: React.FC<{ icon: React.ElementType, title: string, children: React.ReactNode }> = ({ icon: Icon, title, children }) => {
return (
-
+
);
};
const FeaturesSection: React.FC = () => {
return (
-
+
Por que OpenCand?
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index bce67d7..1a5ed4e 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -2,7 +2,7 @@ import React from 'react';
const Footer: React.FC = () => {
return (
-
diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx
new file mode 100644
index 0000000..3513423
--- /dev/null
+++ b/src/components/NotFound.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+const NotFound: React.FC = () => {
+ return (
+
+
+
+
+ 404
+
+
+ Página não encontrada
+
+
+ A página que você está procurando não existe ou foi movida para outro local.
+
+
+
+
+
+ Voltar para a página inicial
+
+
+
+
+ Ou tente uma dessas páginas:
+
+
+
+ Início
+
+ •
+
+ Dados Disponíveis
+
+
+
+
+
+ {/* Decorative elements */}
+
+
+
+ );
+};
+
+export default NotFound;
diff --git a/src/components/StatisticsSection.tsx b/src/components/StatisticsSection.tsx
index 4f8b960..cfaeace 100644
--- a/src/components/StatisticsSection.tsx
+++ b/src/components/StatisticsSection.tsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { openCandApi, type PlatformStats, ApiError } from '../api';
+import Card from '../Components/Card';
interface StatCardProps {
title: string;
@@ -10,17 +11,17 @@ interface StatCardProps {
const StatCard: React.FC = ({ title, value, description, isLoading = false }) => {
return (
-
+
{title}
{isLoading ? (
-
-
+
) : (
{value}
)}
{description}
-
+
);
};
@@ -105,36 +106,26 @@ const StatisticsSection: React.FC = () => {
];
return (
-
-
-
- Dados em Números
-
-
- {statisticsData.slice(0, 3).map((stat, index) => (
-
-
-
+
+
+
+
+ Dados em Números
+
+
+ Estatísticas da nossa plataforma de dados eleitorais
+
+
+
+
+ {statisticsData.map((stat) => (
+
))}
- {statisticsData.length > 3 && (
-
- {statisticsData.slice(3).map((stat, index) => (
-
-
-
- ))}
-
- )}
diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx
deleted file mode 100644
index 8646eff..0000000
--- a/src/components/Tooltip.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { createPortal } from 'react-dom';
-
-interface TooltipProps {
- content: string;
- children: React.ReactNode;
- position?: 'top' | 'bottom' | 'left' | 'right';
- delay?: number;
- className?: string;
-}
-
-const Tooltip: React.FC
= ({
- content,
- children,
- position = 'top',
- delay = 300,
- className = ''
-}) => {
- const [isVisible, setIsVisible] = useState(false);
- const [showTooltip, setShowTooltip] = useState(false);
- const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
- const timeoutRef = useRef(null);
- const tooltipRef = useRef(null);
- const triggerRef = useRef(null);
-
- const calculateTooltipPosition = () => {
- if (!triggerRef.current) return { top: 0, left: 0 };
-
- const triggerRect = triggerRef.current.getBoundingClientRect();
-
- let top = 0;
- let left = 0;
-
- switch (position) {
- case 'top':
- top = triggerRect.top - 8; // 8px offset for arrow and spacing
- left = triggerRect.left + triggerRect.width / 2;
- break;
- case 'bottom':
- top = triggerRect.bottom + 8;
- left = triggerRect.left + triggerRect.width / 2;
- break;
- case 'left':
- top = triggerRect.top + triggerRect.height / 2;
- left = triggerRect.left - 8;
- break;
- case 'right':
- top = triggerRect.top + triggerRect.height / 2;
- left = triggerRect.right + 8;
- break;
- }
-
- return { top, left };
- };
-
- const showTooltipWithDelay = () => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
-
- timeoutRef.current = window.setTimeout(() => {
- const position = calculateTooltipPosition();
- setTooltipPosition(position);
- setIsVisible(true);
- // Small delay to allow for positioning before showing
- setTimeout(() => setShowTooltip(true), 10);
- }, delay);
- };
-
- const hideTooltip = () => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
-
- setShowTooltip(false);
- setTimeout(() => setIsVisible(false), 150); // Match transition duration
- };
-
- // Update tooltip position on scroll or resize
- useEffect(() => {
- if (!isVisible) return;
-
- const updatePosition = () => {
- const position = calculateTooltipPosition();
- setTooltipPosition(position);
- };
-
- const handleScroll = () => updatePosition();
- const handleResize = () => updatePosition();
-
- window.addEventListener('scroll', handleScroll, { passive: true });
- window.addEventListener('resize', handleResize, { passive: true });
-
- return () => {
- window.removeEventListener('scroll', handleScroll);
- window.removeEventListener('resize', handleResize);
- };
- }, [isVisible, position]);
-
- useEffect(() => {
- return () => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
- };
- }, []);
-
- const getTooltipClasses = () => {
- const baseClasses = `
- fixed z-[9999] px-3 py-2 text-sm font-medium text-white bg-gray-900
- rounded-lg shadow-xl backdrop-blur-sm border border-gray-700
- transition-all duration-150 ease-in-out pointer-events-none
- max-w-xs break-words
- `;
-
- const transformClasses = {
- top: '-translate-x-1/2 -translate-y-full',
- bottom: '-translate-x-1/2',
- left: '-translate-x-full -translate-y-1/2',
- right: '-translate-y-1/2'
- };
-
- const visibilityClasses = showTooltip
- ? 'opacity-100 scale-100'
- : 'opacity-0 scale-95';
-
- return `${baseClasses} ${transformClasses[position]} ${visibilityClasses}`;
- };
-
- const getArrowClasses = () => {
- const baseClasses = 'absolute w-2 h-2 bg-gray-900 border border-gray-700 transform rotate-45';
-
- const arrowPositions = {
- top: 'bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 border-t-0 border-l-0',
- bottom: 'top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 border-b-0 border-r-0',
- left: 'right-0 top-1/2 transform translate-x-1/2 -translate-y-1/2 border-l-0 border-b-0',
- right: 'left-0 top-1/2 transform -translate-x-1/2 -translate-y-1/2 border-r-0 border-t-0'
- };
-
- return `${baseClasses} ${arrowPositions[position]}`;
- };
-
- if (!content) {
- return <>{children}>;
- }
-
- return (
- <>
-
- {children}
-
-
- {isVisible && createPortal(
- ,
- document.body
- )}
- >
- );
-};
-
-export default Tooltip;