Some checks failed
Frontend Build and Deploy / build (push) Failing after 11s
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { openCandApi, type PlatformStats, ApiError } from '../api';
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: string;
|
|
description: string;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const StatCard: React.FC<StatCardProps> = ({ title, value, description, isLoading = false }) => {
|
|
return (
|
|
<div className="bg-gray-800/10 backdrop-blur-xs p-6 rounded-lg shadow-xl hover:shadow-indigo-500/30 transform hover:-translate-y-1 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">
|
|
<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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const StatisticsSection: React.FC = () => {
|
|
const [stats, setStats] = useState<PlatformStats | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchStats = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
const platformStats = await openCandApi.getStats();
|
|
setStats(platformStats);
|
|
} catch (err) {
|
|
console.error('Error fetching platform stats:', err);
|
|
if (err instanceof ApiError) {
|
|
setError(`Erro ao carregar estatísticas: ${err.message}`);
|
|
} else {
|
|
setError('Erro inesperado ao carregar estatísticas');
|
|
}
|
|
// Use default stats as fallback
|
|
setStats({
|
|
totalCandidatos: 0,
|
|
totalBemCandidatos: 0,
|
|
totalEleicoes: 0,
|
|
totalValorBemCandidatos: 0,
|
|
totalRedesSociais: 0
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchStats();
|
|
}, []);
|
|
|
|
const formatNumber = (num: number): string => {
|
|
if (num >= 1000000000000) {
|
|
return `${(num / 1000000000000).toFixed(1)} trilhão`;
|
|
} else if (num >= 1000000000) {
|
|
return `${(num / 1000000000).toFixed(1)} bilhão`;
|
|
} else if (num >= 1000000) {
|
|
return `${(num / 1000000).toFixed(1)} milhão`;
|
|
} else if (num >= 1000) {
|
|
return `${(num / 1000).toFixed(0)}K`;
|
|
}
|
|
return num.toLocaleString('pt-BR');
|
|
};
|
|
|
|
const formatCurrency = (value: number): string => {
|
|
return new Intl.NumberFormat('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL',
|
|
notation: 'compact',
|
|
maximumSignificantDigits: 3
|
|
}).format(value);
|
|
};
|
|
|
|
const statisticsData = [
|
|
{
|
|
title: "Total de Candidatos",
|
|
value: isLoading ? "" : `+${formatNumber(stats?.totalCandidatos || 0)}`,
|
|
description: "Registros de candidaturas na plataforma"
|
|
},
|
|
{
|
|
title: "Total de Bens Registrados",
|
|
value: isLoading ? "" : `+${formatNumber(stats?.totalBemCandidatos || 0)}`,
|
|
description: isLoading ? "" : `Somando ${formatCurrency(stats?.totalValorBemCandidatos || 0)} em Patrimônio agregado declarado pelos candidatos`
|
|
},
|
|
{
|
|
title: "Total de Redes Sociais",
|
|
value: isLoading ? "" : formatNumber(stats?.totalRedesSociais || 0),
|
|
description: "Redes sociais conectadas aos candidatos"
|
|
},
|
|
{
|
|
title: "Total de Eleições",
|
|
value: isLoading ? "" : formatNumber(stats?.totalEleicoes || 0),
|
|
description: "Eleições processadas no sistema"
|
|
}
|
|
];
|
|
|
|
return (
|
|
<section id="stats" className="py-20 bg-gray-800/30">
|
|
<div className="container mx-auto px-4">
|
|
<h2 className="text-3xl font-bold text-center text-white mb-12">
|
|
Dados em Números
|
|
</h2>
|
|
<div className="flex flex-wrap justify-center gap-8 mx-auto">
|
|
{statisticsData.slice(0, 3).map((stat, index) => (
|
|
<div key={index} className="w-full md:w-80 lg:w-96">
|
|
<StatCard
|
|
title={stat.title}
|
|
value={stat.value}
|
|
description={stat.description}
|
|
isLoading={isLoading}
|
|
/>
|
|
</div>
|
|
))}
|
|
{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>
|
|
);
|
|
};
|
|
|
|
export default StatisticsSection;
|