feat: adicionar suporte a métricas dinâmicas no componente ProfileCard e integrar ao slide de retrospectiva

main
guilherme 2025-12-11 12:00:11 -03:00
parent 8f4a42eea8
commit 01554466e4
4 changed files with 189 additions and 58 deletions

50
package-lock.json generated
View File

@ -24,6 +24,7 @@
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.26",
"html2canvas": "^1.4.1",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
@ -3487,6 +3488,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz",
@ -3728,6 +3738,15 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -4692,6 +4711,19 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -5944,6 +5976,15 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -6181,6 +6222,15 @@
}
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",

View File

@ -26,6 +26,7 @@
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.26",
"html2canvas": "^1.4.1",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",

View File

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import './ProfileCard.css';
import logoImg from '@/assets/images/agape-logo.png';
@ -39,7 +45,15 @@ const ProfileCardComponent = ({
contactText = 'Contact',
showUserInfo = true,
onContactClick,
// métricas para popular o cartão
totalAcessos = null,
aplicacoesCount = null,
topAppName = null,
topAppAcessos = null,
// permite mostrar/ocultar botões de compartilhamento
showShareButtons = true,
}) => {
const [sharing, setSharing] = useState(false);
const wrapRef = useRef(null);
const shellRef = useRef(null);
@ -346,17 +360,23 @@ const ProfileCardComponent = ({
gap: '16px',
padding: '24px',
}}>
{/* Card de Acessos */}
{/* Card de Acessos (dinâmico) */}
<div className="metric-card bg-gradient-to-br from-purple-900/30 to-blue-900/30">
<div className="metric-content">
<h4 className="metric-value">23.343</h4>
<h4 className="metric-value">
{totalAcessos != null
? totalAcessos.toLocaleString('pt-BR')
: '—'}
</h4>
<p className="metric-label">Acessos</p>
</div>
</div>
<div className="metric-card bg-gradient-to-br from-blue-900/30 to-cyan-900/30">
<div className="metric-content">
<h4 className="metric-value">8</h4>
<h4 className="metric-value">
{aplicacoesCount != null ? aplicacoesCount : '—'}
</h4>
<p className="metric-label">Aplicações</p>
</div>
</div>
@ -365,13 +385,106 @@ const ProfileCardComponent = ({
<div className="metric-content-wide">
<div className="flex items-center gap-2">
<span className="top-badge">TOP 1</span>
<h4 className="metric-app-name">agGestor</h4>
<h4 className="metric-app-name">{topAppName || '—'}</h4>
</div>
<p className="metric-app-desc">
Aplicação mais utilizada
{topAppAcessos != null
? `${topAppAcessos.toLocaleString('pt-BR')} acessos`
: 'Aplicação mais utilizada'}
</p>
</div>
</div>
{/* Botões de compartilhamento */}
{showShareButtons && (
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button
className="btn-primary"
onClick={async () => {
if (navigator.share) {
try {
setSharing(true);
await navigator.share({
title: `Retrospectiva Ágape - ${name}`,
text: `${name}${title}\nAcessos: ${
totalAcessos != null
? totalAcessos.toLocaleString('pt-BR')
: '—'
}`,
url: window.location.href,
});
} catch (err) {
console.error(err);
} finally {
setSharing(false);
}
} else {
const text = encodeURIComponent(
`${name}${title} | Acessos: ${
totalAcessos != null ? totalAcessos : ''
}`,
);
const url = encodeURIComponent(
window.location.href,
);
window.open(
`https://twitter.com/intent/tweet?text=${text}&url=${url}`,
'_blank',
);
}
}}>
{sharing ? 'Compartilhando...' : 'Compartilhar'}
</button>
<button
className="btn-secondary"
onClick={() => {
const text = encodeURIComponent(
`${name}${title} | Acessos: ${
totalAcessos != null ? totalAcessos : ''
}`,
);
const url = encodeURIComponent(window.location.href);
window.open(
`https://www.facebook.com/sharer/sharer.php?u=${url}&quote=${text}`,
'_blank',
);
}}>
Facebook
</button>
<button
className="btn-secondary"
onClick={async () => {
try {
const wrap = wrapRef.current;
if (!wrap) return;
const html2canvas = (await import('html2canvas'))
.default;
const canvas = await html2canvas(wrap, {
backgroundColor: null,
scale: 2,
});
const dataUrl = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = dataUrl;
a.download = `retrospectiva-${
handle || 'usuario'
}.png`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
console.error('Erro ao gerar imagem', err);
alert(
'Não foi possível gerar a imagem. Por favor atualize a página e tente novamente.',
);
}
}}>
Download PNG
</button>
</div>
)}
</div>
</div>
</div>

View File

@ -4,7 +4,11 @@ import {
} from '@/app/hooks/useMetrics';
import logoAImg from '@/assets/images/a-agape.png';
import logoImg from '@/assets/images/agape-logo.png';
import { BuildingIcon, ChartLineUpIcon, ClockIcon, SpinnerIcon } from '@phosphor-icons/react';
import {
BuildingIcon,
ChartLineUpIcon,
SpinnerIcon,
} from '@phosphor-icons/react';
import { motion } from 'framer-motion';
import * as React from 'react';
import { useSearchParams } from 'react-router-dom';
@ -62,6 +66,13 @@ export function RetrospectiveSlides() {
.sort((a, b) => b.totalAcessos - a.totalAcessos)
.slice(0, 5);
const allItems = [...(metricsPortal || []), ...(metricsSystems || [])];
const aplicacoesCount = new Set(
allItems.map((i) => (i.nomeSistema || '').trim()).filter((n) => !!n),
).size;
const topAppName = topItems[0]?.nomeSistema ?? null;
const topAppAcessos = topItems[0]?.totalAcessos ?? null;
return (
<div className="w-full h-screen overflow-hidden">
{isLoading ? (
@ -178,7 +189,7 @@ export function RetrospectiveSlides() {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}>
Portal Corporativo
Acessos no AgPortal
</motion.h2>
<motion.p
@ -242,20 +253,6 @@ export function RetrospectiveSlides() {
Por mês
</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 border border-white/20 text-center">
<div className="text-lg font-bold text-white mb-1">
{Math.round(
metricsPortal.reduce(
(acc, p) => acc + p.totalAcessos,
0,
) / 2920,
)}
</div>
<div className="text-xs text-blue-100 opacity-90">
Por hora*
</div>
</div>
</div>
</motion.div>
</motion.div>
@ -275,9 +272,6 @@ export function RetrospectiveSlides() {
Aplicativos Mais Acessados
</span>
</div>
<p className="text-base text-white/70">
Os sistemas que mais acessaram o portal
</p>
</motion.div>
{/* Lista de sistemas */}
@ -368,44 +362,12 @@ export function RetrospectiveSlides() {
);
})}
</div>
{/* Indicador de progresso */}
<motion.div
className="pt-4"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.8 }}>
<div className="flex flex-col items-center gap-2">
<div className="text-xs text-blue-200 font-medium">
Próxima análise
</div>
<div className="flex items-center gap-2">
{[...Array(3)].map((_, i) => (
<motion.div
key={i}
className="h-1.5 w-1.5 rounded-full bg-white/60"
animate={{
opacity: [0.3, 1, 0.3],
scale: [1, 1.2, 1],
}}
transition={{
duration: 1.5,
repeat: Infinity,
delay: i * 0.2,
}}
/>
))}
</div>
</div>
</motion.div>
</div>
</div>
</div>
</StorySlide>
)}
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="h-full flex flex-col md:flex-row items-center justify-center px-6 gap-12 py-12">
<div className="space-y-8 w-full md:w-1/2 text-center md:text-left">
@ -450,6 +412,11 @@ export function RetrospectiveSlides() {
status="Online"
contactText="Contato"
avatarUrl="https://github.com/GuilhermeSantosUI.png"
// métricas para preencher o cartão final
totalAcessos={totalAcessos}
aplicacoesCount={aplicacoesCount}
topAppName={topAppName}
topAppAcessos={topAppAcessos}
showUserInfo={true}
enableTilt={true}
enableMobileTilt={false}