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
|
# Replace default nginx.conf
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
# Copy our built files into nginx’s html folder
|
# 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
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# (Optional) If you need any custom nginx.conf, COPY it here—
|
# (Optional) If you need any custom nginx.conf, COPY it here—
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<title>OpenCand</title>
|
<title>OpenCand</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 |
54
src/App.css
54
src/App.css
@ -13,7 +13,8 @@ body, html {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom animations */
|
/* Custom animations */
|
||||||
@ -94,4 +95,53 @@ body, html {
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* Internet Explorer and Edge */
|
-ms-overflow-style: none; /* Internet Explorer and Edge */
|
||||||
overflow: -moz-scrollbars-none; /* Old versions of Firefox */
|
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 CandidatePage from './components/CandidatePage/CandidatePage';
|
||||||
import DataStatsPage from './components/DataStatsPage';
|
import DataStatsPage from './components/DataStatsPage';
|
||||||
import StatisticsPage from './components/StatisticsPage';
|
import StatisticsPage from './components/StatisticsPage';
|
||||||
|
import SobrePage from './components/SobrePage';
|
||||||
import NotFound from './components/NotFound';
|
import NotFound from './components/NotFound';
|
||||||
import MatrixBackground from './components/MatrixBackground';
|
import MatrixBackground from './components/MatrixBackground';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@ -16,6 +17,7 @@ import './App.css';
|
|||||||
const HomePage: React.FC = () => (
|
const HomePage: React.FC = () => (
|
||||||
<main className="flex-grow">
|
<main className="flex-grow">
|
||||||
<HeroSection />
|
<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 />
|
<StatisticsSection />
|
||||||
<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="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||||
<FeaturesSection />
|
<FeaturesSection />
|
||||||
@ -34,6 +36,7 @@ function App() {
|
|||||||
<Route path="/candidato/:id" element={<CandidatePage />} />
|
<Route path="/candidato/:id" element={<CandidatePage />} />
|
||||||
<Route path="/dados-disponiveis" element={<DataStatsPage />} />
|
<Route path="/dados-disponiveis" element={<DataStatsPage />} />
|
||||||
<Route path="/estatisticas" element={<StatisticsPage />} />
|
<Route path="/estatisticas" element={<StatisticsPage />} />
|
||||||
|
<Route path="/sobre" element={<SobrePage />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
@ -144,4 +144,23 @@ export interface OpenCandDataAvailabilityStats {
|
|||||||
receitaCandidatos: number[];
|
receitaCandidatos: number[];
|
||||||
redeSocialCandidatos: number[];
|
redeSocialCandidatos: number[];
|
||||||
fotosCandidatos: 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 { BaseApiClient } from './base';
|
||||||
import { API_CONFIG } from '../config/api';
|
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';
|
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');
|
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
|
* Search for candidates by name or other attributes
|
||||||
*/
|
*/
|
||||||
|
@ -11,12 +11,23 @@ interface IncomeExpenseComponentProps {
|
|||||||
isLoadingIncome: boolean;
|
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> = ({
|
const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
|
||||||
expenses,
|
expenses,
|
||||||
income,
|
income,
|
||||||
isLoadingExpenses,
|
isLoadingExpenses,
|
||||||
isLoadingIncome
|
isLoadingIncome
|
||||||
}) => {
|
}) => {
|
||||||
|
const showIncome = hasIncomeData(income);
|
||||||
|
const showExpenses = hasExpenseData(expenses);
|
||||||
|
if (!showIncome && !showExpenses) return null;
|
||||||
return (
|
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="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">
|
<div className="flex items-center mb-6">
|
||||||
@ -25,25 +36,31 @@ const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Income Section */}
|
{/* Income Section */}
|
||||||
<IncomeSection
|
{showIncome && (
|
||||||
income={income}
|
<IncomeSection
|
||||||
isLoadingIncome={isLoadingIncome}
|
income={income}
|
||||||
/>
|
isLoadingIncome={isLoadingIncome}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="flex items-center my-8">
|
{showIncome && showExpenses && (
|
||||||
<div className="flex-grow border-t border-gray-300"></div>
|
<div className="flex items-center my-8">
|
||||||
<div className="flex-shrink-0 px-4">
|
<div className="flex-grow border-t border-gray-300"></div>
|
||||||
<div className="w-3 h-3 bg-gray-300 rounded-full"></div>
|
<div className="flex-shrink-0 px-4">
|
||||||
|
<div className="w-3 h-3 bg-gray-300 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow border-t border-gray-300"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow border-t border-gray-300"></div>
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Expenses Section */}
|
{/* Expenses Section */}
|
||||||
<ExpenseSection
|
{showExpenses && (
|
||||||
expenses={expenses}
|
<ExpenseSection
|
||||||
isLoadingExpenses={isLoadingExpenses}
|
expenses={expenses}
|
||||||
/>
|
isLoadingExpenses={isLoadingExpenses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { FaCloudDownloadAlt, FaGoogleDrive } from 'react-icons/fa';
|
||||||
import { openCandApi } from '../api';
|
import { openCandApi } from '../api';
|
||||||
import type { OpenCandDataAvailabilityStats } from '../api/apiModels';
|
import type { OpenCandDataAvailabilityStats, OpenCandDatabaseStats } from '../api/apiModels';
|
||||||
import Card from '../shared/Card';
|
import Card from '../shared/Card';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
|
|
||||||
const DataStatsPage: React.FC = () => {
|
const DataStatsPage: React.FC = () => {
|
||||||
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
const [stats, setStats] = useState<OpenCandDataAvailabilityStats | null>(null);
|
||||||
|
const [dbStats, setDbStats] = useState<OpenCandDatabaseStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dbLoading, setDbLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [dbError, setDbError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
@ -22,16 +26,28 @@ const DataStatsPage: React.FC = () => {
|
|||||||
setLoading(false);
|
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();
|
fetchStats();
|
||||||
|
fetchDbStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="flex items-center justify-center min-h-[40vh]">
|
||||||
<div className="text-center">
|
<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-16 w-16 border-4 border-indigo-600 border-t-transparent mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-10 w-10 border-4 border-indigo-600 border-t-transparent mb"></div>
|
||||||
<p className="text-lg text-gray-700">Carregando dados...</p>
|
<p className="text-base text-gray-700">Carregando dados...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -68,14 +84,14 @@ const DataStatsPage: React.FC = () => {
|
|||||||
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
|
{ key: 'despesaCandidatos', label: 'Despesas de Candidatos', icon: '💸' },
|
||||||
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
|
{ key: 'receitaCandidatos', label: 'Receitas de Candidatos', icon: '💵' },
|
||||||
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
|
{ key: 'redeSocialCandidatos', label: 'Redes Sociais', icon: '📱' },
|
||||||
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos', icon: '📸' },
|
{ key: 'fotosCandidatos', label: 'Fotos de Candidatos (API)', icon: '📸' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
<div className="min-h-screen py-20 px-4 hover:cursor-default">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* 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">
|
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||||
Disponibilidade de Dados
|
Disponibilidade de Dados
|
||||||
</h1>
|
</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 className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||||
</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 */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||||
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
|
<Card hasAnimation={true} className="bg-gray-800/10 rounded-xl">
|
||||||
@ -133,7 +166,7 @@ const DataStatsPage: React.FC = () => {
|
|||||||
{sortedYears.map((year, index) => (
|
{sortedYears.map((year, index) => (
|
||||||
<th
|
<th
|
||||||
key={year}
|
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` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
{year}
|
{year}
|
||||||
@ -178,6 +211,131 @@ const DataStatsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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';
|
import Card from '../shared/Card';
|
||||||
|
|
||||||
const FeatureCard: React.FC<{ icon: React.ElementType, title: string, children: React.ReactNode }> = ({ icon: Icon, title, children }) => {
|
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 = () => {
|
const FeaturesSection: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<section id="features" className="py-20">
|
<section id="recursos" className="py-20">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<h2 className="text-3xl font-bold text-center text-white mb-12">
|
<h2 className="text-4xl md:text-5xl font-bold text-center text-white mb-12">
|
||||||
Por que OpenCand?
|
Recursos
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-wrap justify-center gap-8">
|
<div className="flex flex-wrap justify-center gap-8">
|
||||||
<FeatureCard icon={DocumentTextIcon} title="Acesso Simplificado">
|
<FeatureCard icon={DocumentTextIcon} title="Acesso Simplificado">
|
||||||
Navegue facilmente por dados complexos do TSE com uma interface limpa e amigável.
|
Navegue facilmente por dados complexos do TSE com uma interface limpa e amigável.
|
||||||
</FeatureCard>
|
</FeatureCard>
|
||||||
<FeatureCard icon={ChartBarIcon} title="Visualizações Claras">
|
<FeatureCard icon={IdentificationIcon} title="Visualizações Claras">
|
||||||
Entenda as tendências e padrões com gráficos e resumos visuais dos dados eleitorais.
|
Visualização detalhada de perfis, redes sociais e histórico eleitoral.
|
||||||
</FeatureCard>
|
</FeatureCard>
|
||||||
<FeatureCard icon={LightBulbIcon} title="Insights Valiosos">
|
<FeatureCard icon={DocumentMagnifyingGlassIcon} title="Declaração de Bens">
|
||||||
Obtenha informações relevantes sobre candidatos, partidos e financiamento de campanhas.
|
Visualização acerca de bens declarados para cada candidato.
|
||||||
</FeatureCard>
|
</FeatureCard>
|
||||||
<FeatureCard icon={BookOpenIcon} title="Open Source">
|
<FeatureCard icon={MagnifyingGlassIcon} title="Informações de Campanha">
|
||||||
Contribua para um projeto aberto e transparente, ajudando a melhorar a plataforma para todos.
|
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>
|
||||||
<FeatureCard icon={ArrowDownOnSquareStackIcon} title="Dados Abertos">
|
<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.
|
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 = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
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">
|
<div className="container mx-auto">
|
||||||
<p className="mb-2">
|
<p className="mb-2 text-md">
|
||||||
© {new Date().getFullYear()} OpenCand. Todos os direitos reservados.
|
OpenCand
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-xs">
|
||||||
Democratizando o acesso à informação eleitoral.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -6,7 +6,7 @@ const HeroSection: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<section
|
<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"
|
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="absolute inset-0 bg-black/60"></div>
|
||||||
<div className="relative z-10 text-center max-w-6xl">
|
<div className="relative z-10 text-center max-w-6xl">
|
||||||
@ -26,6 +26,18 @@ const HeroSection: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,14 +10,15 @@ const Navbar: React.FC = () => {
|
|||||||
<NavbarMatrixBackground />
|
<NavbarMatrixBackground />
|
||||||
<div className="relative z-10 p-4">
|
<div className="relative z-10 p-4">
|
||||||
<div className="container mx-auto flex justify-between items-center">
|
<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">
|
<a href="/" className="flex items-center gap-2 text-2xl font-bold text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||||
OpenCand
|
<img src="/assets/opencand-line.png" alt="OpenCand logo" className="h-8 w-auto" />
|
||||||
|
<span>OpenCand</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Button href="/dados-disponiveis">Dados Disponíveis</Button>
|
<Button href="/dados-disponiveis">Dados Disponíveis</Button>
|
||||||
<Button href="/estatisticas">Estatíscas</Button>
|
<Button href="/estatisticas">Estatíscas</Button>
|
||||||
<Button href="/#features">Recursos</Button>
|
<Button href="/#recursos">Recursos</Button>
|
||||||
<Button href="/about">Sobre</Button>
|
<Button href="/sobre">Sobre</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<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-600">Patrimônio Inicial (<span className="text-xs text-gray-500">{enrichmentData.anoInicial}</span>):</span>
|
||||||
<span className="text-gray-900 font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<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-600">Patrimônio Final (<span className="text-xs text-gray-500">{enrichmentData.anoFinal}</span>):</span>
|
||||||
<span className="text-gray-900 font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 pt-3">
|
<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 ? 'text-green-600' : 'text-red-600'
|
||||||
}`}>
|
}`}>
|
||||||
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
{enrichmentData.enriquecimento >= 0 ? '+' : ''}
|
||||||
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR') || '0'}
|
R$ {enrichmentData.enriquecimento?.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0,00'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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-gray-900">{item.ano || 'N/A'}</td>
|
||||||
<td className="py-3 text-right font-medium text-gray-900">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -66,6 +66,9 @@ const StatisticsPage: React.FC = () => {
|
|||||||
<p className="text-white/70 text-lg">
|
<p className="text-white/70 text-lg">
|
||||||
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
Análise de dados e estatísticas dos candidatos e partidos brasileiros
|
||||||
</p>
|
</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 className="w-32 h-1 bg-gradient-to-r from-indigo-400 to-purple-400 mx-auto mt-6 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -26,82 +26,4 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@apply bg-gray-900 text-white;
|
@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;
|
hasAnimation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const RandomCandButton: React.FC<RandomCandButtonProps> = ({
|
const RandomCandButton: React.FC<RandomCandButtonProps> = ({
|
||||||
className = '',
|
className = '',
|
||||||
hasAnimation = false
|
hasAnimation = false
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
const handleRandomCandidate = async () => {
|
const handleRandomCandidate = async () => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const randomCandidate = await openCandApi.getRandomCandidate();
|
const randomCandidate = await openCandApi.getRandomCandidate();
|
||||||
navigate(`/candidato/${randomCandidate.idCandidato}`);
|
navigate(`/candidato/${randomCandidate.idCandidato}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar candidato aleatório:', error);
|
console.error('Erro ao buscar candidato aleatório:', error);
|
||||||
// You might want to show a toast notification or error message to the user
|
// You might want to show a toast notification or error message to the user
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRandomCandidate}
|
onClick={handleRandomCandidate}
|
||||||
className={`flex items-center ${className}`}
|
className={`flex items-center relative overflow-hidden ${className}`}
|
||||||
hasAnimation={hasAnimation}
|
hasAnimation={hasAnimation}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<ArrowPathIcon className="h-4 w-4 mr-2" />
|
<ArrowPathIcon
|
||||||
Candidato aleatório
|
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' }}
|
||||||
|
>
|
||||||
|
Candidato aleatório
|
||||||
|
</span>
|
||||||
</Button>
|
</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)
|
* @param cpf - A CPF string (11 digits without punctuation)
|
||||||
* @returns Formatted CPF with masking or the original input if invalid
|
* @returns Formatted CPF with masking or the original input if invalid
|
||||||
*/
|
*/
|
||||||
export function maskCpf(cpf: string): string {
|
export function maskCpf(cpf: string): string {
|
||||||
// Clean input, keeping only digits
|
|
||||||
const cleanCpf = cpf.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Validate if it's 11 digits
|
// Validate if it's 11 digits
|
||||||
if (cleanCpf.length !== 11) {
|
if (cpf.length !== 11) {
|
||||||
return cpf;
|
return cpf;
|
||||||
}
|
}
|
||||||
|
// Mask with standard punctuation: 123.456.789-10
|
||||||
// Format with mask: 123.***.789-10
|
return `${cpf.slice(0, 3)}.${cpf.slice(3, 6)}.${cpf.slice(6, 9)}-${cpf.slice(9)}`;
|
||||||
return `${cleanCpf.slice(0, 3)}.***.${cleanCpf.slice(6, 9)}-${cleanCpf.slice(9)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user