Compare commits
6 Commits
885b8a6599
...
46e76acad3
Author | SHA1 | Date | |
---|---|---|---|
46e76acad3 | |||
ceaffc088e | |||
af7bd64617 | |||
c347d5ce24 | |||
7acbc48f43 | |||
7512f42e2f |
@ -20,6 +20,7 @@ RUN rm -rf /usr/share/nginx/html/*
|
||||
# Replace default nginx.conf
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
# Copy our built files into nginx’s html folder
|
||||
COPY ./public/assets /usr/share/nginx/html
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# (Optional) If you need any custom nginx.conf, COPY it here—
|
||||
|
@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<title>OpenCand</title>
|
||||
</head>
|
||||
<body>
|
||||
|
BIN
public/assets/Congresso_Nacional_hero.jpg
Normal file
BIN
public/assets/Congresso_Nacional_hero.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 352 KiB |
BIN
public/assets/opencand-line.png
Normal file
BIN
public/assets/opencand-line.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
52
src/App.css
52
src/App.css
@ -13,7 +13,8 @@ body, html {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@ -95,3 +96,52 @@ body, html {
|
||||
-ms-overflow-style: none; /* Internet Explorer and Edge */
|
||||
overflow: -moz-scrollbars-none; /* Old versions of Firefox */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Custom minimal scrollbar styles */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(107, 114, 128, 0.7);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Custom fade-in animation for dropdown content */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import Footer from './components/Footer';
|
||||
import CandidatePage from './components/CandidatePage/CandidatePage';
|
||||
import DataStatsPage from './components/DataStatsPage';
|
||||
import StatisticsPage from './components/StatisticsPage';
|
||||
import SobrePage from './components/SobrePage';
|
||||
import NotFound from './components/NotFound';
|
||||
import MatrixBackground from './components/MatrixBackground';
|
||||
import './App.css';
|
||||
@ -16,6 +17,7 @@ import './App.css';
|
||||
const HomePage: React.FC = () => (
|
||||
<main className="flex-grow">
|
||||
<HeroSection />
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-12 rounded-full"></div>
|
||||
<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 />
|
||||
@ -34,6 +36,7 @@ function App() {
|
||||
<Route path="/candidato/:id" element={<CandidatePage />} />
|
||||
<Route path="/dados-disponiveis" element={<DataStatsPage />} />
|
||||
<Route path="/estatisticas" element={<StatisticsPage />} />
|
||||
<Route path="/sobre" element={<SobrePage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
@ -145,3 +145,22 @@ export interface OpenCandDataAvailabilityStats {
|
||||
redeSocialCandidatos: number[];
|
||||
fotosCandidatos: number[];
|
||||
}
|
||||
|
||||
export interface OpenCandDatabaseStats {
|
||||
tables: {
|
||||
name: string;
|
||||
totalSize: number; // in bytes
|
||||
entries: number; // number of rows
|
||||
}[];
|
||||
materializedViews: {
|
||||
name: string;
|
||||
totalSize: number; // in bytes
|
||||
entries: number; // number of rows
|
||||
}[];
|
||||
indexes: {
|
||||
amount: number; // number of indexes
|
||||
size: number; // total size of indexes in bytes
|
||||
};
|
||||
totalSize: number; // total size of the database in bytes
|
||||
totalEntries: number; // total number of entries across all tables
|
||||
}
|
@ -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, RandomCandidate } from './apiModels';
|
||||
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, OpenCandDataAvailabilityStats, OpenCandDatabaseStats, PlatformStats, RandomCandidate } from './apiModels';
|
||||
import type { EnrichmentResponse, StatisticsConfig, ValueSumRequest, ValueSumResponse } from './apiStatisticsModels';
|
||||
|
||||
/**
|
||||
@ -27,6 +27,13 @@ export class OpenCandApi extends BaseApiClient {
|
||||
return this.get<OpenCandDataAvailabilityStats>('/v1/stats/data-availability');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database tech stats
|
||||
*/
|
||||
async getDatabaseTechStats(): Promise<OpenCandDatabaseStats> {
|
||||
return this.get<OpenCandDatabaseStats>(`/v1/stats/tech`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for candidates by name or other attributes
|
||||
*/
|
||||
|
@ -11,12 +11,23 @@ interface IncomeExpenseComponentProps {
|
||||
isLoadingIncome: boolean;
|
||||
}
|
||||
|
||||
const hasIncomeData = (income: CandidateIncome | null) => {
|
||||
if (!income || income.receitas.length === 0) return false;
|
||||
};
|
||||
|
||||
const hasExpenseData = (expenses: CandidateExpenses | null) => {
|
||||
if (!expenses || expenses.despesas.length === 0) return false;
|
||||
};
|
||||
|
||||
const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
|
||||
expenses,
|
||||
income,
|
||||
isLoadingExpenses,
|
||||
isLoadingIncome
|
||||
}) => {
|
||||
const showIncome = hasIncomeData(income);
|
||||
const showExpenses = hasExpenseData(expenses);
|
||||
if (!showIncome && !showExpenses) return 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">
|
||||
@ -25,12 +36,15 @@ const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Income Section */}
|
||||
{showIncome && (
|
||||
<IncomeSection
|
||||
income={income}
|
||||
isLoadingIncome={isLoadingIncome}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{showIncome && showExpenses && (
|
||||
<div className="flex items-center my-8">
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
<div className="flex-shrink-0 px-4">
|
||||
@ -38,12 +52,15 @@ const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
|
||||
</div>
|
||||
<div className="flex-grow border-t border-gray-300"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expenses Section */}
|
||||
{showExpenses && (
|
||||
<ExpenseSection
|
||||
expenses={expenses}
|
||||
isLoadingExpenses={isLoadingExpenses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaCloudDownloadAlt, FaGoogleDrive } from 'react-icons/fa';
|
||||
import { openCandApi } from '../api';
|
||||
import type { OpenCandDataAvailabilityStats } from '../api/apiModels';
|
||||
import type { OpenCandDataAvailabilityStats, OpenCandDatabaseStats } from '../api/apiModels';
|
||||
import Card from '../shared/Card';
|
||||
import ErrorPage from './ErrorPage';
|
||||
|
||||
const DataStatsPage: React.FC = () => {
|
||||
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
||||
const [dbStats, setDbStats] = useState<OpenCandDatabaseStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dbLoading, setDbLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
@ -22,16 +26,28 @@ const DataStatsPage: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDbStats = async () => {
|
||||
try {
|
||||
setDbLoading(true);
|
||||
const dbData = await openCandApi.getDatabaseTechStats();
|
||||
setDbStats(dbData);
|
||||
} catch (err) {
|
||||
setDbError('Erro ao carregar estatísticas técnicas do banco de dados');
|
||||
console.error('Error fetching database tech stats:', err);
|
||||
} finally {
|
||||
setDbLoading(false);
|
||||
}
|
||||
};
|
||||
fetchStats();
|
||||
fetchDbStats();
|
||||
}, []);
|
||||
|
||||
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 className="flex items-center justify-center min-h-[40vh]">
|
||||
<div className="backdrop-blur-md bg-white/90 rounded-2xl shadow-xl p-8 max-w-md w-full border border-white/30 flex flex-col items-center">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-4 border-indigo-600 border-t-transparent mb"></div>
|
||||
<p className="text-base text-gray-700">Carregando dados...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -68,14 +84,14 @@ const DataStatsPage: React.FC = () => {
|
||||
{ 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: '📸' },
|
||||
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos (API)', icon: '📸' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<div className="text-center mb-12 animate-slide-in-left">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||
Disponibilidade de Dados
|
||||
</h1>
|
||||
@ -85,6 +101,23 @@ const DataStatsPage: React.FC = () => {
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Download DB Dump Section */}
|
||||
<div className="flex justify-center mb-10 animate-slide-in-left backdrop-blur-xs bg-gray-800/10 rounded-xl shadow-lg hover:shadow-xl transform transition-all duration-200 overflow-hidden ring-1 ring-gray-700 hover:ring-indigo-300 px-8 py-6 flex flex-col md:flex-row items-center gap-4 max-w-xl-auto">
|
||||
<div className="flex items-center gap-3 mb-2 md:mb-0">
|
||||
<FaCloudDownloadAlt className="text-3xl text-green-400" />
|
||||
<span className="text-lg font-semibold text-white">Download do Dump do Banco de Dados</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://drive.google.com/file/d/1cfMItrsAdv8y8YUNp04D33s6pYrRbmDn/view?usp=sharing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 py-2 px-5 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"
|
||||
>
|
||||
<FaGoogleDrive className="text-xl" />
|
||||
Google Drive
|
||||
</a>
|
||||
</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">
|
||||
@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => {
|
||||
{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"
|
||||
className="text-center p-4 text-white font-semibold border-b border-gray-700/30 animate-slide-in-left"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{year}
|
||||
@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Tech Stats Section */}
|
||||
<div className="mt-20 flex justify-center">
|
||||
<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 mb-12 w-full max-w-2xl">
|
||||
<div className="p-6 border-b border-gray-700/30 bg-gray-800/10">
|
||||
<h2 className="text-2xl font-bold text-white">Dados Técnicas do Banco de Dados</h2>
|
||||
<p className="text-gray-400 mt-2">Informações sobre tabelas, views materializadas e índices</p>
|
||||
</div>
|
||||
{dbLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-4 border-indigo-600 border-t-transparent mr-4"></div>
|
||||
<span className="text-gray-300">Carregando dados do banco de dados...</span>
|
||||
</div>
|
||||
) : dbError ? (
|
||||
<div className="p-8 text-red-400">{dbError}</div>
|
||||
) : dbStats ? (
|
||||
<div className="p-6 space-y-10">
|
||||
{/* Tables */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Tabelas</h3>
|
||||
<div className="overflow-x-auto max-h-112" style={{maxHeight: '32rem'}}>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dbStats.tables.map((table) => {
|
||||
const name = table.name.replace(/^public\./, '');
|
||||
const sizeMB = table.totalSize / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr key={table.name} className="hover:bg-gray-800/10 transition-all duration-200">
|
||||
<td className="p-3 text-white">{name}</td>
|
||||
<td className="p-3 text-gray-300">{sizeDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{table.entries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* Total row */}
|
||||
{(() => {
|
||||
const totalSize = dbStats.tables.reduce((acc, t) => acc + t.totalSize, 0);
|
||||
const totalEntries = dbStats.tables.reduce((acc, t) => acc + t.entries, 0);
|
||||
const totalMB = totalSize / 1024 / 1024;
|
||||
const totalDisplay = totalMB > 1024
|
||||
? `${(totalMB / 1024).toFixed(2)} GB`
|
||||
: `${totalMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr className="font-bold bg-gray-900/30">
|
||||
<td className="p-3 text-white">Total</td>
|
||||
<td className="p-3 text-gray-300">{totalDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{totalEntries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Materialized Views */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Views Materializadas</h3>
|
||||
<div className="overflow-x-auto max-h-96" style={{maxHeight: '28rem'}}>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Nome</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Entradas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dbStats.materializedViews.map((view) => {
|
||||
const name = view.name.replace(/^public\./, '');
|
||||
const sizeMB = view.totalSize / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return (
|
||||
<tr key={view.name} className="hover:bg-gray-800/10 transition-all duration-200">
|
||||
<td className="p-3 text-white">{name}</td>
|
||||
<td className="p-3 text-gray-300">{sizeDisplay}</td>
|
||||
<td className="p-3 text-gray-300">{view.entries.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Indexes */}
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Índices</h3>
|
||||
<div className="overflow-x-auto max-h-96 flex justify-center" style={{maxHeight: '28rem'}}>
|
||||
<table className="w-auto text-left border-collapse mx-auto">
|
||||
<thead>
|
||||
<tr className="bg-gray-800/10">
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Quantidade</th>
|
||||
<th className="p-3 text-white font-semibold border-b border-gray-700/30">Tamanho Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-3 text-gray-300">{dbStats.indexes.amount}</td>
|
||||
{(() => {
|
||||
const sizeMB = dbStats.indexes.size / 1024 / 1024;
|
||||
const sizeDisplay = sizeMB > 1024
|
||||
? `${(sizeMB / 1024).toFixed(2)} GB`
|
||||
: `${sizeMB.toFixed(2)} MB`;
|
||||
return <td className="p-3 text-gray-300">{sizeDisplay}</td>;
|
||||
})()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ArrowDownOnSquareStackIcon, BookOpenIcon, ChartBarIcon, DocumentTextIcon, LightBulbIcon } from '@heroicons/react/24/outline';
|
||||
import { ArrowDownOnSquareStackIcon, BookOpenIcon, ChartBarIcon, ChartBarSquareIcon, DocumentMagnifyingGlassIcon, DocumentTextIcon, IdentificationIcon, LightBulbIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import Card from '../shared/Card';
|
||||
|
||||
const FeatureCard: React.FC<{ icon: React.ElementType, title: string, children: React.ReactNode }> = ({ icon: Icon, title, children }) => {
|
||||
@ -14,23 +14,26 @@ const FeatureCard: React.FC<{ icon: React.ElementType, title: string, children:
|
||||
|
||||
const FeaturesSection: React.FC = () => {
|
||||
return (
|
||||
<section id="features" className="py-20">
|
||||
<section id="recursos" 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?
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center text-white mb-12">
|
||||
Recursos
|
||||
</h2>
|
||||
<div className="flex flex-wrap justify-center gap-8">
|
||||
<FeatureCard icon={DocumentTextIcon} title="Acesso Simplificado">
|
||||
Navegue facilmente por dados complexos do TSE com uma interface limpa e amigável.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={ChartBarIcon} title="Visualizações Claras">
|
||||
Entenda as tendências e padrões com gráficos e resumos visuais dos dados eleitorais.
|
||||
<FeatureCard icon={IdentificationIcon} title="Visualizações Claras">
|
||||
Visualização detalhada de perfis, redes sociais e histórico eleitoral.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={LightBulbIcon} title="Insights Valiosos">
|
||||
Obtenha informações relevantes sobre candidatos, partidos e financiamento de campanhas.
|
||||
<FeatureCard icon={DocumentMagnifyingGlassIcon} title="Declaração de Bens">
|
||||
Visualização acerca de bens declarados para cada candidato.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={BookOpenIcon} title="Open Source">
|
||||
Contribua para um projeto aberto e transparente, ajudando a melhorar a plataforma para todos.
|
||||
<FeatureCard icon={MagnifyingGlassIcon} title="Informações de Campanha">
|
||||
Análise de receitas e despesas de campanha quando disponível.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={ChartBarSquareIcon} title="Estatísticas">
|
||||
Estatísticas e gráficos interativos para entender melhor o cenário eleitoral.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={ArrowDownOnSquareStackIcon} title="Dados Abertos">
|
||||
Os dados são acessíveis através do TSE e também disponibilizados em nosso repositório GitHub, garantindo transparência e confiabilidade.
|
||||
|
@ -2,13 +2,15 @@ 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-4 text-center backdrop-blur-xs shadow-lg ring-1 ring-gray-800">
|
||||
<div className="container mx-auto">
|
||||
<p className="mb-2">
|
||||
© {new Date().getFullYear()} OpenCand. Todos os direitos reservados.
|
||||
<p className="mb-2 text-md">
|
||||
OpenCand
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Democratizando o acesso à informação eleitoral.
|
||||
<p className="text-xs">
|
||||
Não nos responsabilizamos por quaisquer erros ou inconsistências nos dados apresentados, pois estes são de livre interpretação da plataforma e devem ser verificados com os dados oficiais do TSE.
|
||||
<br />
|
||||
Logos desenhados por Freepik.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -6,7 +6,7 @@ const HeroSection: React.FC = () => {
|
||||
return (
|
||||
<section
|
||||
className="min-h-screen flex flex-col justify-center items-center text-white bg-cover bg-center bg-no-repeat bg-gray-900 relative"
|
||||
style={{ backgroundImage: "url('https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/National_Congress_of_Brazil%2C_Brasilia.jpg/1024px-National_Congress_of_Brazil%2C_Brasilia.jpg')" }}
|
||||
style={{ backgroundImage: "url('/assets/Congresso_Nacional_hero.jpg')" }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/60"></div>
|
||||
<div className="relative z-10 text-center max-w-6xl">
|
||||
@ -26,6 +26,18 @@ const HeroSection: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Image credit */}
|
||||
<span className="absolute left-4 bottom-2 z-20 text-[10px] text-gray-200 bg-black/30 px-1.5 py-0.5 rounded select-none pointer-events-none opacity-55">
|
||||
Edilson Rodrigues/Agência Senado. Senado Federal,
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by/2.0"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-1 text-gray-200 hover:text-white"
|
||||
>
|
||||
CC BY 2.0
|
||||
</a>, via Wikimedia Commons
|
||||
</span>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -10,14 +10,15 @@ const Navbar: React.FC = () => {
|
||||
<NavbarMatrixBackground />
|
||||
<div className="relative z-10 p-4">
|
||||
<div className="container mx-auto flex justify-between items-center">
|
||||
<a href="/" className="text-2xl font-bold text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||
OpenCand
|
||||
<a href="/" className="flex items-center gap-2 text-2xl font-bold text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||
<img src="/assets/opencand-line.png" alt="OpenCand logo" className="h-8 w-auto" />
|
||||
<span>OpenCand</span>
|
||||
</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>
|
||||
<Button href="/#recursos">Recursos</Button>
|
||||
<Button href="/sobre">Sobre</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
120
src/components/SobrePage.tsx
Normal file
120
src/components/SobrePage.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { FaGithub, FaLinkedin } from 'react-icons/fa';
|
||||
|
||||
const SobrePage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[80vh] py-12 px-4">
|
||||
<div
|
||||
className="backdrop-blur-md bg-white/90 rounded-2xl shadow-xl p-8 max-w-3xl w-full border border-white/30"
|
||||
style={{ boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.2)' }}
|
||||
>
|
||||
<div className="space-y-4 text-gray-700">
|
||||
<p className="text-lg leading-relaxed">
|
||||
O <strong>OpenCand</strong> é uma plataforma que visa explorar de forma intuitiva os dados eleitorais brasileiros e possui
|
||||
o objetivo de ser uma alternativa para o acesso às informações públicas do Tribunal Superior Eleitoral (TSE).
|
||||
</p>
|
||||
|
||||
<p className="leading-relaxed">
|
||||
O projeto foi desenvolvido por <strong className="inline-flex items-center gap-1">
|
||||
ivanch
|
||||
<a
|
||||
href="https://github.com/ivanch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="align-middle inline-block hover:scale-110 transition-transform"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<FaGithub className="text-gray-700 hover:text-black" size={16} />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/joseivanch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="align-middle inline-block hover:scale-110 transition-transform"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<FaLinkedin className="text-blue-700 hover:text-blue-900" size={16} />
|
||||
</a>
|
||||
</strong> como hobby, o código é semi-aberto e está disponível no <strong>GitHub</strong>.
|
||||
Atualmente apenas o ETL (Extract-Transform-Load) está disponível, juntamente com o banco de dados final.
|
||||
</p>
|
||||
|
||||
<p className="leading-relaxed">
|
||||
A API também pode ser utilizada para acessar os dados de forma pública, porém ainda não está documentada. A documentação da API será disponibilizada em breve.
|
||||
</p>
|
||||
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||
|
||||
<p className="leading-relaxed">
|
||||
Não nos responsabilizamos por quaisquer erros ou inconsistências nos dados apresentados, pois estes são de livre interpretação da plataforma e devem ser verificados com os dados oficiais do TSE.
|
||||
<br />
|
||||
A plataforma é uma iniciativa independente e não possui qualquer vínculo com o TSE ou órgãos governamentais.
|
||||
</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 className="mt-6 flex flex-col items-center">
|
||||
<p className="font-semibold text-gray-800 mb-2 text-center">Links úteis:</p>
|
||||
<ul className="space-y-2 w-full max-w-md">
|
||||
<li className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 bg-indigo-500 rounded-full mr-3"></span>
|
||||
<a
|
||||
href="https://divulgacandcontas.tse.jus.br/divulga/#/home"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-700 hover:underline hover:text-indigo-900 transition-colors text-center"
|
||||
>
|
||||
DivulgaCand do TSE
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 bg-indigo-500 rounded-full mr-3"></span>
|
||||
<a
|
||||
href="https://sig.tse.jus.br/ords/dwapr/r/seai/sig-eleicao/home"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-700 hover:underline hover:text-indigo-900 transition-colors text-center"
|
||||
>
|
||||
Estatísticas do TSE
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 bg-indigo-500 rounded-full mr-3"></span>
|
||||
<a
|
||||
href="https://dadosabertos.tse.jus.br/dataset/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-700 hover:underline hover:text-indigo-900 transition-colors text-center"
|
||||
>
|
||||
Dataset do TSE
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||
|
||||
<p className="leading-relaxed">
|
||||
Contato: {[
|
||||
'opencand',
|
||||
<span key="at" style={{ userSelect: 'text' }}>@</span>,
|
||||
'ivanch',
|
||||
<span key="dot" style={{ userSelect: 'text' }}>.</span>,
|
||||
'me'
|
||||
]}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-gray-300">
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
Desenvolvido com .NET, PostgreSQL, React (TypeScript/Tailwind CSS) e dados abertos do TSE.<br />
|
||||
<span className="text-xs">
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SobrePage;
|
@ -95,14 +95,14 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span>
|
||||
<span className="text-gray-900 font-medium">
|
||||
R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.patrimonioInicial?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span>
|
||||
<span className="text-gray-900 font-medium">
|
||||
R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.patrimonioFinal?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
@ -112,7 +112,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
enrichmentData.enriquecimento >= 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@ const StatisticsGraphs: React.FC<StatisticsGraphsProps> = ({
|
||||
)}
|
||||
<td className="py-3 text-gray-900">{item.ano || 'N/A'}</td>
|
||||
<td className="py-3 text-right font-medium text-gray-900">
|
||||
R$ {item.valor?.toLocaleString('pt-BR') || '0'}
|
||||
R$ {item.valor?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {
|
||||
<p className="text-white/70 text-lg">
|
||||
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
||||
</p>
|
||||
<p className="text-white/70 text-lg">
|
||||
Para mais informações, acesse a <a href="https://sig.tse.jus.br/ords/dwapr/r/seai/sig-eleicao/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">página de estatísticas do TSE</a> ou o <a href="https://divulgacandcontas.tse.jus.br/divulga/#/home" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">DivulgaCand do TSE</a>.
|
||||
</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>
|
||||
|
||||
|
@ -27,81 +27,3 @@ body {
|
||||
|
||||
@apply bg-gray-900 text-white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
} */
|
||||
|
||||
/* Custom minimal scrollbar styles */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(107, 114, 128, 0.7);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Ensure full width layout */
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Custom fade-in animation for dropdown content */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
@ -9,30 +9,44 @@ interface RandomCandButtonProps {
|
||||
hasAnimation?: boolean;
|
||||
}
|
||||
|
||||
|
||||
const RandomCandButton: React.FC<RandomCandButtonProps> = ({
|
||||
className = '',
|
||||
hasAnimation = false
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleRandomCandidate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const randomCandidate = await openCandApi.getRandomCandidate();
|
||||
navigate(`/candidato/${randomCandidate.idCandidato}`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar candidato aleatório:', error);
|
||||
// You might want to show a toast notification or error message to the user
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleRandomCandidate}
|
||||
className={`flex items-center ${className}`}
|
||||
className={`flex items-center relative overflow-hidden ${className}`}
|
||||
hasAnimation={hasAnimation}
|
||||
disabled={loading}
|
||||
>
|
||||
<ArrowPathIcon
|
||||
className={`h-4 w-4 transition-transform duration-500 ${loading ? 'animate-spin' : 'mr-2'}`}
|
||||
style={{ zIndex: 2 }}
|
||||
/>
|
||||
<span
|
||||
className={`inline-block transition-all duration-300 origin-left whitespace-nowrap ${loading ? 'scale-x-0 max-w-0' : 'scale-x-100 max-w-xs'}`}
|
||||
style={{ zIndex: 1, transitionProperty: 'transform, max-width' }}
|
||||
>
|
||||
<ArrowPathIcon className="h-4 w-4 mr-2" />
|
||||
Candidato aleatório
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
@ -1,19 +1,15 @@
|
||||
/**
|
||||
* Formats a CPF string with masking (123.***.789-10)
|
||||
* Formats a CPF string (might include masking) and masks it for display.
|
||||
* @param cpf - A CPF string (11 digits without punctuation)
|
||||
* @returns Formatted CPF with masking or the original input if invalid
|
||||
*/
|
||||
export function maskCpf(cpf: string): string {
|
||||
// Clean input, keeping only digits
|
||||
const cleanCpf = cpf.replace(/\D/g, '');
|
||||
|
||||
// Validate if it's 11 digits
|
||||
if (cleanCpf.length !== 11) {
|
||||
if (cpf.length !== 11) {
|
||||
return cpf;
|
||||
}
|
||||
|
||||
// Format with mask: 123.***.789-10
|
||||
return `${cleanCpf.slice(0, 3)}.***.${cleanCpf.slice(6, 9)}-${cleanCpf.slice(9)}`;
|
||||
// Mask with standard punctuation: 123.456.789-10
|
||||
return `${cpf.slice(0, 3)}.${cpf.slice(3, 6)}.${cpf.slice(6, 9)}-${cpf.slice(9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user