Compare commits
No commits in common. "91e2448a123e6ac2ba41985eb1e938ea4274621e" and "83ff2131f792b0ab6b4fe751c5c6418dcbe80d73" have entirely different histories.
91e2448a12
...
83ff2131f7
45
src/App.css
45
src/App.css
@ -26,51 +26,6 @@ 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;
|
||||
|
@ -6,8 +6,6 @@ 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';
|
||||
|
||||
@ -16,7 +14,6 @@ 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>
|
||||
);
|
||||
@ -31,8 +28,6 @@ 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 />
|
||||
|
@ -124,11 +124,3 @@ export interface Income {
|
||||
valor: number;
|
||||
}
|
||||
|
||||
export interface OpenCandDataAvailabilityStats {
|
||||
candidatos: number[];
|
||||
bemCandidatos: number[];
|
||||
despesaCandidatos: number[];
|
||||
receitaCandidatos: number[];
|
||||
redeSocialCandidatos: number[];
|
||||
fotosCandidatos: number[];
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { BaseApiClient } from './base';
|
||||
import { API_CONFIG } from '../config/api';
|
||||
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, PlatformStats } from './apiModels';
|
||||
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, PlatformStats } from './apiModels';
|
||||
|
||||
/**
|
||||
* OpenCand API client for interacting with the OpenCand platform
|
||||
@ -18,14 +18,6 @@ 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
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useParams, useNavigate } 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,7 +7,6 @@ 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 }>();
|
||||
@ -117,13 +116,12 @@ 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>
|
||||
<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"
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Voltar para a página inicial
|
||||
</Link>
|
||||
|
||||
Voltar ao Início
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@ -133,14 +131,13 @@ 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 mb-4"
|
||||
hasAnimation={false}
|
||||
className="flex items-center text-white hover:text-gray-300 transition-colors mb-4"
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5 mr-2" />
|
||||
Voltar à busca
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
{candidateDetails && (
|
||||
<h1 className="text-3xl font-bold text-white">{candidateDetails.nome}</h1>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
||||
import { type Election } from '../../api';
|
||||
import Tooltip from '../../Components/Tooltip';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
interface ElectionsComponentProps {
|
||||
elections: Election[] | null;
|
||||
|
@ -21,36 +21,35 @@ interface SocialMediaComponentProps {
|
||||
}
|
||||
|
||||
// Helper function to get social media icons
|
||||
const getSocialMediaIcon = (rede: string, isRecent: boolean = true): React.ReactElement => {
|
||||
const getSocialMediaIcon = (rede: string): React.ReactElement => {
|
||||
const iconClass = "h-5 w-5 mr-2";
|
||||
const opacityClass = isRecent ? "" : "opacity-50";
|
||||
|
||||
switch (rede.toLowerCase()) {
|
||||
case 'facebook':
|
||||
return <FaFacebook className={`${iconClass} ${opacityClass} text-blue-600`} />;
|
||||
return <FaFacebook className={`${iconClass} text-blue-600`} />;
|
||||
case 'instagram':
|
||||
return <FaInstagram className={`${iconClass} ${opacityClass} text-pink-600`} />;
|
||||
return <FaInstagram className={`${iconClass} text-pink-600`} />;
|
||||
case 'x/twitter':
|
||||
case 'twitter':
|
||||
return <FaXTwitter className={`${iconClass} ${opacityClass} text-black`} />;
|
||||
return <FaXTwitter className={`${iconClass} text-black`} />;
|
||||
case 'tiktok':
|
||||
return <FaTiktok className={`${iconClass} ${opacityClass} text-black`} />;
|
||||
return <FaTiktok className={`${iconClass} text-black`} />;
|
||||
case 'youtube':
|
||||
return <FaYoutube className={`${iconClass} ${opacityClass} text-red-600`} />;
|
||||
return <FaYoutube className={`${iconClass} text-red-600`} />;
|
||||
case 'linkedin':
|
||||
return <FaLinkedin className={`${iconClass} ${opacityClass} text-blue-700`} />;
|
||||
return <FaLinkedin className={`${iconClass} text-blue-700`} />;
|
||||
case 'whatsapp':
|
||||
return <FaWhatsapp className={`${iconClass} ${opacityClass} text-green-600`} />;
|
||||
return <FaWhatsapp className={`${iconClass} text-green-600`} />;
|
||||
case 'threads':
|
||||
return <FaThreads className={`${iconClass} ${opacityClass} text-black`} />;
|
||||
return <FaThreads className={`${iconClass} text-black`} />;
|
||||
case 'telegram':
|
||||
return <FaTelegram className={`${iconClass} ${opacityClass} text-blue-500`} />;
|
||||
return <FaTelegram className={`${iconClass} text-blue-500`} />;
|
||||
case 'spotify':
|
||||
return <FaSpotify className={`${iconClass} ${opacityClass} text-green-500`} />;
|
||||
return <FaSpotify className={`${iconClass} text-green-500`} />;
|
||||
case 'kwai':
|
||||
case 'outros':
|
||||
default:
|
||||
return <FaLink className={`${iconClass} ${opacityClass} text-gray-600`} />;
|
||||
return <FaLink className={`${iconClass} text-gray-600`} />;
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,11 +57,6 @@ 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">
|
||||
@ -76,41 +70,27 @@ const SocialMediaComponent: React.FC<SocialMediaComponentProps> = ({
|
||||
</div>
|
||||
) : redesSociais && redesSociais.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{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>
|
||||
{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>
|
||||
</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">
|
||||
|
@ -1,186 +0,0 @@
|
||||
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;
|
@ -1,20 +1,19 @@
|
||||
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 (
|
||||
<Card>
|
||||
<div className="bg-gray-800/30 p-6 rounded-lg backdrop-blur-xs shadow-xl ring-1 ring-gray-700 max-w-md">
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturesSection: React.FC = () => {
|
||||
return (
|
||||
<section id="features" className="py-20">
|
||||
<section id="features" 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">
|
||||
Por que OpenCand?
|
||||
|
@ -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 backdrop-blur-xs shadow-lg ring-1 ring-gray-800">
|
||||
<footer className="bg-gray-800/30 text-gray-400 py-8 text-center">
|
||||
<div className="container mx-auto">
|
||||
<p className="mb-2">
|
||||
© {new Date().getFullYear()} OpenCand. Todos os direitos reservados.
|
||||
|
31
src/components/NavButton.tsx
Normal file
31
src/components/NavButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
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;
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from '../Components/Button';
|
||||
import NavButton from './NavButton';
|
||||
import NavbarMatrixBackground from './NavbarMatrixBackground';
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
@ -14,10 +14,9 @@ const Navbar: React.FC = () => {
|
||||
OpenCand
|
||||
</a>
|
||||
<div className="space-x-4">
|
||||
<Button href="/dados-disponiveis">Dados Disponíveis</Button>
|
||||
<Button href="/estatisticas">Estatíscas</Button>
|
||||
<Button href="/#features">Recursos</Button>
|
||||
<Button href="/about">Sobre</Button>
|
||||
<NavButton href="#stats">Estatíscas</NavButton>
|
||||
<NavButton href="#features">Recursos</NavButton>
|
||||
<NavButton href="/about">Sobre</NavButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,59 +0,0 @@
|
||||
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;
|
@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { openCandApi, type PlatformStats, ApiError } from '../api';
|
||||
import Card from '../Components/Card';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
@ -11,17 +10,17 @@ interface StatCardProps {
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = ({ title, value, description, isLoading = false }) => {
|
||||
return (
|
||||
<Card hasAnimation={true} disableCursor={true} height={11} width={20}>
|
||||
<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">
|
||||
<h3 className="text-indigo-400 text-xl font-semibold mb-2">{title}</h3>
|
||||
{isLoading ? (
|
||||
<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 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>
|
||||
) : (
|
||||
<p className="text-4xl font-bold text-white mb-3">{value}</p>
|
||||
)}
|
||||
<p className="text-gray-400 text-sm">{description}</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -106,26 +105,36 @@ const StatisticsSection: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user