melhorias no design

This commit is contained in:
José Henrique 2025-06-10 13:19:00 -03:00
parent 83ff2131f7
commit b141218adf
17 changed files with 506 additions and 293 deletions

View File

@ -26,6 +26,51 @@ body, html {
animation: fadeIn 0.3s ease-in-out forwards;
}
/* New animations for DataStatsPage */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-in-left {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out forwards;
}
.animate-fade-in-up {
animation: fade-in-up 0.8s ease-out forwards;
}
.animate-slide-in-left {
animation: slide-in-left 0.6s ease-out forwards;
}
.delay-100 {
animation-delay: 0.1s;
}
.delay-200 {
animation-delay: 0.2s;
}
/* Custom scrollbar for search results */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;

View File

@ -6,6 +6,8 @@ import StatisticsSection from './components/StatisticsSection';
import FeaturesSection from './components/FeaturesSection';
import Footer from './components/Footer';
import CandidatePage from './components/CandidatePage/CandidatePage';
import DataStatsPage from './components/DataStatsPage';
import NotFound from './components/NotFound';
import MatrixBackground from './components/MatrixBackground';
import './App.css';
@ -14,6 +16,7 @@ const HomePage: React.FC = () => (
<main className="flex-grow">
<HeroSection />
<StatisticsSection />
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
<FeaturesSection />
</main>
);
@ -28,6 +31,8 @@ function App() {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/candidato/:id" element={<CandidatePage />} />
<Route path="/dados-disponiveis" element={<DataStatsPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<Footer />

View File

@ -124,3 +124,11 @@ export interface Income {
valor: number;
}
export interface OpenCandDataAvailabilityStats {
candidatos: number[];
bemCandidatos: number[];
despesaCandidatos: number[];
receitaCandidatos: number[];
redeSocialCandidatos: number[];
fotosCandidatos: number[];
}

View File

@ -1,6 +1,6 @@
import { BaseApiClient } from './base';
import { API_CONFIG } from '../config/api';
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, PlatformStats } from './apiModels';
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, PlatformStats } from './apiModels';
/**
* OpenCand API client for interacting with the OpenCand platform
@ -18,6 +18,14 @@ export class OpenCandApi extends BaseApiClient {
return this.get<PlatformStats>('/v1/stats');
}
/**
* Get platform data stats
* GET /v1/stats
*/
async getDataAvailabilityStats(): Promise<OpenCandDataAvailabilityStats> {
return this.get<OpenCandDataAvailabilityStats>('/v1/stats/data-availability');
}
/**
* Search for candidates by name or other attributes
*/

66
src/components/Button.tsx Normal file
View File

@ -0,0 +1,66 @@
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
className?: string;
hasAnimation?: boolean;
disableCursor?: boolean;
onClick?: () => void;
href?: string;
type?: 'button' | 'submit' | 'reset';
}
const Button: React.FC<ButtonProps> = ({
children,
className = "",
hasAnimation = true,
disableCursor = false,
onClick,
href,
type = 'button'
}) => {
const animationClasses = hasAnimation ? `hover:shadow-xl
transform
hover:scale-[1.01]`
: "";
const cursorClasses = disableCursor ? "hover:cursor-default"
: "hover:cursor-pointer";
const baseClasses = `bg-gray-800/30
px-4 py-2
rounded-full
backdrop-blur-xs
shadow-xl
ring-1
transition-all
duration-200
hover:ring-indigo-400/20
hover:bg-gray-700/40
ring-gray-900
text-white
transition-colors
${animationClasses} ${cursorClasses} ${className}`;
if (href) {
return (
<a
href={href}
className={baseClasses}
>
{children}
</a>
);
}
return (
<button
type={type}
onClick={onClick}
className={baseClasses}
>
{children}
</button>
);
};
export default Button;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
import { openCandApi, type CandidateDetails, type CandidateAssets, type CandidateRedesSociais, type CandidateExpenses, type CandidateIncome, ApiError } from '../../api';
import ElectionsComponent from './ElectionsComponent';
@ -7,6 +7,7 @@ import AssetsComponent from './AssetsComponent';
import BasicCandidateInfoComponent from './BasicCandidateInfoComponent';
import SocialMediaComponent from './SocialMediaComponent';
import IncomeExpenseComponent from './IncomeExpenseComponent';
import Button from '../../Components/Button';
const CandidatePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@ -116,12 +117,13 @@ const CandidatePage: React.FC = () => {
<div className="text-center text-white">
<h1 className="text-2xl font-bold mb-4">Erro</h1>
<p className="text-red-400 mb-4">{error}</p>
<button
onClick={() => navigate('/')}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
<Link
to="/"
className="inline-block px-8 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-600 hover:to-purple-700 transition-all duration-300 transform shadow-lg hover:shadow-xl"
>
Voltar ao Início
</button>
Voltar para a página inicial
</Link>
</div>
</main>
);
@ -131,13 +133,14 @@ const CandidatePage: React.FC = () => {
<main className="flex-grow p-6 max-w-7xl mx-auto">
{/* Header with back button */}
<div className="mb-6">
<button
<Button
onClick={() => navigate('/')}
className="flex items-center text-white hover:text-gray-300 transition-colors mb-4"
className="flex items-center text-white mb-4"
hasAnimation={false}
>
<ArrowLeftIcon className="h-5 w-5 mr-2" />
Voltar à busca
</button>
</Button>
{candidateDetails && (
<h1 className="text-3xl font-bold text-white">{candidateDetails.nome}</h1>

View File

@ -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;

View File

@ -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 <FaFacebook className={`${iconClass} text-blue-600`} />;
return <FaFacebook className={`${iconClass} ${opacityClass} text-blue-600`} />;
case 'instagram':
return <FaInstagram className={`${iconClass} text-pink-600`} />;
return <FaInstagram className={`${iconClass} ${opacityClass} text-pink-600`} />;
case 'x/twitter':
case 'twitter':
return <FaXTwitter className={`${iconClass} text-black`} />;
return <FaXTwitter className={`${iconClass} ${opacityClass} text-black`} />;
case 'tiktok':
return <FaTiktok className={`${iconClass} text-black`} />;
return <FaTiktok className={`${iconClass} ${opacityClass} text-black`} />;
case 'youtube':
return <FaYoutube className={`${iconClass} text-red-600`} />;
return <FaYoutube className={`${iconClass} ${opacityClass} text-red-600`} />;
case 'linkedin':
return <FaLinkedin className={`${iconClass} text-blue-700`} />;
return <FaLinkedin className={`${iconClass} ${opacityClass} text-blue-700`} />;
case 'whatsapp':
return <FaWhatsapp className={`${iconClass} text-green-600`} />;
return <FaWhatsapp className={`${iconClass} ${opacityClass} text-green-600`} />;
case 'threads':
return <FaThreads className={`${iconClass} text-black`} />;
return <FaThreads className={`${iconClass} ${opacityClass} text-black`} />;
case 'telegram':
return <FaTelegram className={`${iconClass} text-blue-500`} />;
return <FaTelegram className={`${iconClass} ${opacityClass} text-blue-500`} />;
case 'spotify':
return <FaSpotify className={`${iconClass} text-green-500`} />;
return <FaSpotify className={`${iconClass} ${opacityClass} text-green-500`} />;
case 'kwai':
case 'outros':
default:
return <FaLink className={`${iconClass} text-gray-600`} />;
return <FaLink className={`${iconClass} ${opacityClass} text-gray-600`} />;
}
};
@ -57,6 +58,11 @@ const SocialMediaComponent: React.FC<SocialMediaComponentProps> = ({
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 (
<div className="bg-white/95 backdrop-blur-sm rounded-xl shadow-lg hover:shadow-xl transform hover:scale-[1.01] transition-all duration-200 p-6">
<div className="flex items-center mb-6">
@ -70,27 +76,41 @@ const SocialMediaComponent: React.FC<SocialMediaComponentProps> = ({
</div>
) : redesSociais && redesSociais.length > 0 ? (
<div className="space-y-3">
{redesSociais.map((redeSocial: RedeSocial, index: number) => (
<div key={`${redeSocial.idCandidato}-${redeSocial.rede}-${index}`} className="border border-gray-200 rounded-lg p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center mb-2">
{getSocialMediaIcon(redeSocial.rede)}
<span className="font-semibold text-gray-900 mr-2">{redeSocial.rede}</span>
<span className="text-sm text-gray-500">({redeSocial.ano})</span>
{redesSociais.map((redeSocial: RedeSocial, index: number) => {
const isRecent = redeSocial.ano === mostRecentYear;
return (
<div
key={`${redeSocial.idCandidato}-${redeSocial.rede}-${index}`}
className={`border border-gray-200 rounded-lg p-3 hover:bg-gray-50 transition-colors ${
isRecent ? '' : 'opacity-95 hover:bg-gray-100'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center mb-2">
{getSocialMediaIcon(redeSocial.rede, isRecent)}
<span className={`font-semibold mr-2 ${isRecent ? 'text-gray-900' : 'text-gray-500'}`}>
{redeSocial.rede}
</span>
<span className="text-sm text-gray-500">({redeSocial.ano})</span>
</div>
<a
href={redeSocial.link}
target="_blank"
rel="noopener noreferrer"
className={`text-sm break-all transition-colors ${
isRecent
? 'text-blue-600 hover:text-blue-800'
: 'text-blue-400 hover:text-blue-600'
}`}
>
{redeSocial.link}
</a>
</div>
<a
href={redeSocial.link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm break-all transition-colors"
>
{redeSocial.link}
</a>
</div>
</div>
</div>
))}
);
})}
</div>
) : (
<div className="text-center text-gray-500 py-8">

31
src/components/Card.tsx Normal file
View File

@ -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<CardProps> = ({ 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 (
<div
className={`bg-gray-800/30 p-6 rounded-lg backdrop-blur-xs shadow-xl ring-1 ring-gray-700 max-w-md ${animationClasses} ${cursorClasses} ${className}`}
style={sizeStyles}
>
{children}
</div>
);
};
export default Card;

View File

@ -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<OpenCandDataAvailabilityStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-indigo-600 border-t-transparent mx-auto mb-4"></div>
<p className="text-lg text-gray-700">Carregando dados...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="text-red-600 text-6xl mb-4"></div>
<p className="text-lg text-gray-700">{error}</p>
</div>
</div>
);
}
if (!stats) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-lg text-gray-700">Nenhum dado disponível</p>
</div>
);
}
// Get all unique years from all data types
const allYears = new Set<number>();
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 (
<div className="min-h-screen py-20 px-4">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-12 animate-fade-in">
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
Disponibilidade de Dados
</h1>
<p className="text-xl text-gray-400">
Visualize a disponibilidade dos dados por ano em nossa base
</p>
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
<div className="text-center">
<div className="text-3xl mb-2">📊</div>
<div className="text-2xl font-bold text-white">{dataTypes.length}</div>
<div className="text-gray-400">Tipos de Dados</div>
</div>
</Card>
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
<div className="text-center">
<div className="text-3xl mb-2">📅</div>
<div className="text-2xl font-bold text-white">{sortedYears.length}</div>
<div className="text-gray-400">Anos Disponíveis</div>
</div>
</Card>
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
<div className="text-center">
<div className="text-3xl mb-2">🗓</div>
<div className="text-2xl font-bold text-white">
{sortedYears.length > 0 ? `${Math.min(...sortedYears)} - ${Math.max(...sortedYears)}` : 'N/A'}
</div>
<div className="text-gray-400">Período</div>
</div>
</Card>
</div>
{/* Data Availability Table */}
<div className="bg-gray-800/10 backdrop-blur-xs rounded-xl shadow-lg hover:shadow-xl transform hover:scale-[1.005] transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300">
<div className="p-6 border-b border-gray-700/30 bg-gray-800/10">
<h2 className="text-2xl font-bold text-white">
Matriz de Disponibilidade
</h2>
<p className="text-gray-400 mt-2">
Disponível Não Disponível
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-800/10">
<th className="text-left p-4 text-white font-semibold border-b border-gray-700/30 sticky left-0 bg-gray-800/10">
Tipo de Dado
</th>
{sortedYears.map((year, index) => (
<th
key={year}
className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-fade-in"
style={{ animationDelay: `${index * 50}ms` }}
>
{year}
</th>
))}
</tr>
</thead>
<tbody>
{dataTypes.map((dataType, rowIndex) => (
<tr
key={dataType.key}
className="hover:bg-gray-800/10 transition-all duration-300 animate-slide-in-left"
style={{ animationDelay: `${rowIndex * 100}ms` }}
>
<td className="p-4 border-b border-gray-700/20 text-white sticky left-0 bg-gray-800/10">
<div className="flex items-center space-x-3">
<span className="text-xl">{dataType.icon}</span>
<span>{dataType.label}</span>
</div>
</td>
{sortedYears.map((year, cellIndex) => {
const isAvailable = (stats[dataType.key as keyof OpenCandDataAvailabilityStats] as number[]).includes(year);
return (
<td
key={year}
className="text-center p-4 border-b border-gray-700/20 animate-fade-in"
style={{ animationDelay: `${(rowIndex * sortedYears.length + cellIndex) * 30}ms` }}
>
<div className={`inline-flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300 ${
isAvailable
? 'bg-green-500/20 text-green-300 hover:bg-green-500/30 hover:scale-110 hover:cursor-default'
: 'bg-red-500/20 text-red-300 hover:bg-red-500/30 hover:cursor-default'
}`}>
{isAvailable ? '✅' : '❌'}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export default DataStatsPage;

View File

@ -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 (
<div className="bg-gray-800/30 p-6 rounded-lg backdrop-blur-xs shadow-xl ring-1 ring-gray-700 max-w-md">
<Card>
<Icon className="h-10 w-10 text-indigo-400 mb-4 mx-auto" />
<h3 className="text-xl font-semibold text-white mb-2">{title}</h3>
<p className="text-gray-400">{children}</p>
</div>
</Card>
);
};
const FeaturesSection: React.FC = () => {
return (
<section id="features" className="py-20 bg-gray-800/30">
<section id="features" className="py-20">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center text-white mb-12">
Por que OpenCand?

View File

@ -2,7 +2,7 @@ import React from 'react';
const Footer: React.FC = () => {
return (
<footer className="bg-gray-800/30 text-gray-400 py-8 text-center">
<footer className="bg-gray-800/30 text-gray-400 py-8 text-center backdrop-blur-xs shadow-lg ring-1 ring-gray-800">
<div className="container mx-auto">
<p className="mb-2">
&copy; {new Date().getFullYear()} OpenCand. Todos os direitos reservados.

View File

@ -1,31 +0,0 @@
import React from 'react';
interface NavButtonProps {
href: string;
children: React.ReactNode;
className?: string;
}
const NavButton: React.FC<NavButtonProps> = ({ href, children, className = '' }) => {
return (
<a
href={href}
className={`
inline-block px-4 py-2
rounded-full
backdrop-blur-sm
bg-gray-800/30
text-gray-100
hover:bg-gray-700/40
hover:text-white
transition-all duration-300 ease-in-out
cursor-pointer
${className}
`.trim()}
>
{children}
</a>
);
};
export default NavButton;

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import NavButton from './NavButton';
import Button from '../Components/Button';
import NavbarMatrixBackground from './NavbarMatrixBackground';
const Navbar: React.FC = () => {
@ -14,9 +14,10 @@ const Navbar: React.FC = () => {
OpenCand
</a>
<div className="space-x-4">
<NavButton href="#stats">Estatíscas</NavButton>
<NavButton href="#features">Recursos</NavButton>
<NavButton href="/about">Sobre</NavButton>
<Button href="/dados-disponiveis">Dados Disponíveis</Button>
<Button href="/estatisticas">Estatíscas</Button>
<Button href="/#features">Recursos</Button>
<Button href="/about">Sobre</Button>
</div>
</div>
</div>

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Link } from 'react-router-dom';
const NotFound: React.FC = () => {
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="text-center">
<div className="mb-8">
<h1 className="text-9xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400 mb-4">
404
</h1>
<h2 className="text-3xl font-semibold text-white mb-4">
Página não encontrada
</h2>
<p className="text-gray-300 text-lg mb-8 max-w-md mx-auto">
A página que você está procurando não existe ou foi movida para outro local.
</p>
</div>
<div className="space-y-4">
<Link
to="/"
className="inline-block px-8 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-600 hover:to-purple-700 transition-all duration-300 transform shadow-lg hover:shadow-xl"
>
Voltar para a página inicial
</Link>
<div className="mt-6">
<p className="text-gray-400 text-sm">
Ou tente uma dessas páginas:
</p>
<div className="flex justify-center space-x-4 mt-3">
<Link
to="/"
className="text-indigo-400 hover:text-indigo-300 transition-colors duration-200"
>
Início
</Link>
<span className="text-gray-500"></span>
<Link
to="/dados-disponiveis"
className="text-indigo-400 hover:text-indigo-300 transition-colors duration-200"
>
Dados Disponíveis
</Link>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 -z-10">
<div className="w-96 h-96 bg-gradient-to-r from-indigo-500/10 to-purple-500/10 rounded-full blur-3xl"></div>
</div>
</div>
</div>
);
};
export default NotFound;

View File

@ -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<StatCardProps> = ({ title, value, description, isLoading = false }) => {
return (
<div className="bg-gray-800/10 backdrop-blur-xs p-6 rounded-lg shadow-xl ring-1 ring-gray-700 hover:shadow-indigo-500/30 transform hover:-translate-y-1 hover:ring-1 hover:ring-white/10 hover:scale-[1.01] transition-all duration-300">
<Card hasAnimation={true} disableCursor={true} height={11} width={20}>
<h3 className="text-indigo-400 text-xl font-semibold mb-2">{title}</h3>
{isLoading ? (
<div className="h-12 flex items-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<div className="h-12 flex items-center justify-center">
<div className="animate-spin rounded-full border-4 border-indigo-600 border-t-transparent"></div>
</div>
) : (
<p className="text-4xl font-bold text-white mb-3">{value}</p>
)}
<p className="text-gray-400 text-sm">{description}</p>
</div>
</Card>
);
};
@ -105,36 +106,26 @@ const StatisticsSection: React.FC = () => {
];
return (
<section id="stats" className="py-20 bg-gray-800/30">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center text-white mb-12">
Dados em Números
</h2>
<div className="flex flex-wrap justify-center gap-8 mx-auto">
{statisticsData.slice(0, 3).map((stat, index) => (
<div key={index} className="w-full md:w-80 lg:w-96">
<StatCard
title={stat.title}
value={stat.value}
description={stat.description}
isLoading={isLoading}
/>
</div>
<section id="stats" className="py-20">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center mb-12 animate-fade-in">
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">
Dados em Números
</h2>
<p className="text-xl text-gray-400">
Estatísticas da nossa plataforma de dados eleitorais
</p>
</div>
<div className="flex flex-wrap justify-center gap-8">
{statisticsData.map((stat) => (
<StatCard
title={stat.title}
value={stat.value}
description={stat.description}
isLoading={isLoading}
/>
))}
{statisticsData.length > 3 && (
<div className="w-full flex flex-wrap justify-center gap-8">
{statisticsData.slice(3).map((stat, index) => (
<div key={index + 3} className="w-full md:w-80 lg:w-96">
<StatCard
title={stat.title}
value={stat.value}
description={stat.description}
isLoading={isLoading}
/>
</div>
))}
</div>
)}
</div>
</div>
</section>

View File

@ -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<TooltipProps> = ({
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<number | null>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLDivElement>(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 (
<>
<div
ref={triggerRef}
className={`relative inline-block ${className}`}
onMouseEnter={showTooltipWithDelay}
onMouseLeave={hideTooltip}
onFocus={showTooltipWithDelay}
onBlur={hideTooltip}
>
{children}
</div>
{isVisible && createPortal(
<div
ref={tooltipRef}
className={getTooltipClasses()}
style={{
top: tooltipPosition.top,
left: tooltipPosition.left,
}}
role="tooltip"
aria-hidden={!showTooltip}
>
{content}
<div className={getArrowClasses()} />
</div>,
document.body
)}
</>
);
};
export default Tooltip;