feat: implement login slide and metrics hooks, add API token handling

main
guilherme 2025-12-11 11:11:54 -03:00
parent 24cf52f6da
commit 5a8a97e9b4
6 changed files with 604 additions and 301 deletions

View File

@ -1,307 +1,15 @@
import logoAImg from '@/assets/images/a-agape.png';
import logoImg from '@/assets/images/agape-logo.png';
import { motion } from 'framer-motion';
import ProfileCard from './views/components/ProfileCard';
import {
BuildingIcon,
CalendarIcon,
CertificateIcon,
ClockIcon,
} from '@phosphor-icons/react';
import { useState } from 'react';
import { summary } from './views/components/mock/summary';
import { Navigation } from './views/components/navigation';
import { StorySlide } from './views/components/story-slide';
import { Button } from './views/components/ui/button';
import { Card } from './views/components/ui/card';
import { Badge } from './views/components/ui/badge';
import { Navigate, Route, Routes } from 'react-router-dom';
import { LoginSlide } from './views/components/login-slide';
import { RetrospectiveSlides } from './views/components/retrospective-slides';
export function App() {
return (
<div className="w-full h-screen overflow-hidden">
<Retrospectiva />
</div>
<Routes>
<Route path="/" element={<LoginSlide />} />
<Route path="/retrospectiva" element={<RetrospectiveSlides />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
export default function Retrospectiva() {
const total = 6;
const [current, setCurrent] = useState(0);
return (
<div className="h-screen w-full overflow-hidden bg-black">
<Navigation total={total} current={current} setCurrent={setCurrent} />
<div className="h-full snap-y snap-mandatory overflow-y-scroll scroll-smooth">
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="text-center space-y-8">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<img
src={logoImg}
alt="Ágape Logo"
className="mx-auto w-52 mb-6"
/>
</motion.div>
<motion.h1
className="text-6xl md:text-7xl font-medium mb-4"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
Sua Retrospectiva
<br />
Ágape {summary.year}
</motion.h1>
<motion.p
className="text-xl md:text-2xl text-white/90 max-w-2xl mx-auto font-light"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}>
Um ano de conquistas, eficiência e transparência na gestão pública
</motion.p>
<motion.div
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.6 }}>
<Button
size="lg"
className="bg-white text-[#0e233d] hover:bg-white/90 text-lg px-8 py-6">
Começar
</Button>
</motion.div>
<motion.div
className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.8 }}>
<span>Transparência</span>
<span></span>
<span>Eficiência</span>
<span></span>
<span>Parceria</span>
</motion.div>
</div>
</StorySlide>
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="space-y-8">
<div className="text-center space-y-4">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<BuildingIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Você utilizou
</h2>
<motion.div
className="text-7xl md:text-8xl font-bold text-[#145190]"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.8, delay: 0.2 }}>
{summary.numApplicationsUsed}
</motion.div>
<p className="text-2xl text-muted-foreground">
aplicações Ágape neste ano
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-12">
{summary.topApplications.map((app, index) => (
<motion.div
key={index}
className="p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<h3 className="text-xl font-semibold text-[#0e233d]">
{app.name}
</h3>
<p className="text-3xl font-bold text-[#145190] mt-2">
{app.uses} usos
</p>
</motion.div>
))}
</div>
</div>
</StorySlide>
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="space-y-8">
<div className="text-center space-y-4">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<CalendarIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Principais Ações
</h2>
<p className="text-xl text-muted-foreground">
As atividades que mais impactaram sua gestão
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{summary.topActions.map((action, index) => (
<motion.div
key={action.action}
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<Card className="p-6 hover:shadow-lg transition-shadow h-full">
<div className="flex flex-col h-full justify-between">
<div>
<div className="text-5xl font-bold text-[#145190] mb-2">
{index + 1}
</div>
<h3 className="font-semibold text-lg mb-2">
{action.action}
</h3>
</div>
<div className="text-3xl font-bold text-[#0e233d] mt-4">
{action.count.toLocaleString('pt-BR')}
</div>
</div>
</Card>
</motion.div>
))}
</div>
</div>
</StorySlide>
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center min-h-screen p-8">
<motion.div
className="space-y-8 text-center lg:text-left"
initial={{ x: -50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<ClockIcon className="h-24 w-24 mb-8 text-yellow-300 lg:mx-0" />
</motion.div>
<div className="space-y-6">
<h2 className="text-4xl md:text-5xl font-bold">
Seu horário preferido
</h2>
<motion.div
className="text-6xl md:text-7xl font-bold text-yellow-300"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.8, delay: 0.2 }}>
{summary.favoriteHourRange}
</motion.div>
<p className="text-xl text-white/90 max-w-lg">
É quando você mais acessa as plataformas Ágape
</p>
</div>
</motion.div>
<motion.div
className="flex justify-center lg:justify-end"
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
<Card className="bg-white/10 backdrop-blur border-white/20 text-white p-8 max-w-md w-full">
<CertificateIcon className="h-14 w-14 mx-auto mb-6 text-yellow-300" />
<h3 className="text-2xl font-bold mb-4 text-center">
{summary.badge.title}
</h3>
<div className="flex justify-center mb-4">
<Badge
variant="secondary"
className="bg-yellow-300 text-[#0e233d] px-4 py-2 text-lg">
{summary.badge.subtitle}
</Badge>
</div>
<p className="text-white/90 text-center text-lg">
{summary.badge.description}
</p>
</Card>
</motion.div>
</div>
</StorySlide>
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="h-screen max-h-screen overflow-y-auto flex flex-col md:flex-row items-center justify-center px-6 gap-12">
<div className="space-y-12 w-full md:w-1/2 text-center md:text-left">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<img src={logoAImg} className="w-10" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
A Ágape deseja a você um {summary.year + 1} repleto de sucesso!
</h2>
<p className="text-xl text-muted-foreground max-w-xl">
{summary.messageFinal}
</p>
<motion.div
className="text-sm text-muted-foreground"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}>
<p>
&copy; {summary.year} {summary.orgName}
</p>
<p className="mt-2">O Futuro da Gestão Pública começa aqui!</p>
</motion.div>
</div>
<div className="w-full md:w-1/2 flex justify-center">
<div className="max-w-md w-full">
<ProfileCard
name="Guilherme Santos"
title="Software Engineer"
handle="javicodes"
status="Online"
contactText="Contact Me"
avatarUrl="https://github.com/GuilhermeSantosUI.png"
showUserInfo={true}
enableTilt={true}
enableMobileTilt={false}
onContactClick={() => console.log('Contact clicked')}
/>
</div>
</div>
</div>
</StorySlide>
</div>
</div>
);
}
export default App;

View File

@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { metricsService } from '../services/metrics';
export function useMetricsByPortal(cpf: string, ano: number) {
return useQuery({
queryKey: ['metricsByPortal', cpf, ano],
queryFn: () => metricsService.getMetricsByPortal({ cpf, ano }),
enabled: !!cpf && !!ano,
});
}
export function useMetricsBySystems(cpf: string, ano: number) {
return useQuery({
queryKey: ['metricsBySystems', cpf, ano],
queryFn: () => metricsService.getMetricsBySystems({ cpf, ano }),
enabled: !!cpf && !!ano,
});
}

View File

@ -6,6 +6,12 @@ const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
// Adicionar token temporariamente aos headers
const token = import.meta.env.VITE_API_TOKEN || '';
if (token) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@ -0,0 +1,148 @@
import logoImg from '@/assets/images/agape-logo.png';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from './ui/button';
import { Input } from './ui/input';
export function LoginSlide() {
const [cpf, setCpf] = useState('');
const [ano, setAno] = useState(new Date().getFullYear().toString());
const [errors, setErrors] = useState<{ cpf?: string; ano?: string }>({});
const navigate = useNavigate();
const validateCPF = (cpfValue: string) => {
const cleanCPF = cpfValue.replace(/\D/g, '');
return cleanCPF.length === 11;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: { cpf?: string; ano?: string } = {};
if (!cpf || !validateCPF(cpf)) {
newErrors.cpf = 'CPF inválido (deve ter 11 dígitos)';
}
if (
!ano ||
parseInt(ano) < 2020 ||
parseInt(ano) > new Date().getFullYear()
) {
newErrors.ano = `Ano deve estar entre 2020 e ${new Date().getFullYear()}`;
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Navega para a retrospectiva com os parâmetros
navigate(`/retrospectiva?cpf=${cpf.replace(/\D/g, '')}&ano=${ano}`);
};
const formatCPF = (value: string) => {
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length <= 11) {
return cleanValue
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})/, '$1-$2');
}
return value;
};
return (
<div className="h-screen w-full bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white flex items-center justify-center p-4">
<motion.div
className="w-full max-w-md space-y-8"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6 }}>
<div className="text-center space-y-6">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}>
<img src={logoImg} alt="Ágape Logo" className="mx-auto w-40" />
</motion.div>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}>
<h1 className="text-4xl font-bold">Sua Retrospectiva</h1>
<p className="text-xl text-white/80 mt-2">Ágape {ano}</p>
</motion.div>
</div>
<motion.form
onSubmit={handleSubmit}
className="space-y-6 bg-white/10 backdrop-blur-sm p-8 rounded-lg border border-white/20"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.4 }}>
<div className="space-y-2">
<label htmlFor="cpf" className="block text-sm font-medium">
CPF
</label>
<Input
id="cpf"
type="text"
placeholder="000.000.000-00"
value={cpf}
onChange={(e) => {
setCpf(formatCPF(e.target.value));
if (errors.cpf) {
setErrors({ ...errors, cpf: undefined });
}
}}
className="bg-white/20 border-white/30 text-white placeholder:text-white/50"
/>
{errors.cpf && <p className="text-red-300 text-sm">{errors.cpf}</p>}
</div>
<div className="space-y-2">
<label htmlFor="ano" className="block text-sm font-medium">
Ano
</label>
<Input
id="ano"
type="number"
min="2020"
max={new Date().getFullYear()}
value={ano}
onChange={(e) => {
setAno(e.target.value);
if (errors.ano) {
setErrors({ ...errors, ano: undefined });
}
}}
className="bg-white/20 border-white/30 text-white"
/>
{errors.ano && <p className="text-red-300 text-sm">{errors.ano}</p>}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}>
<Button
type="submit"
className="w-full bg-white text-[#0e233d] hover:bg-white/90 font-semibold">
Ver Minha Retrospectiva
</Button>
</motion.div>
</motion.form>
<motion.p
className="text-center text-white/60 text-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.6 }}>
Seus dados são seguros e processados com transparência
</motion.p>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,402 @@
import {
useMetricsByPortal,
useMetricsBySystems,
} from '@/app/hooks/useMetrics';
import logoAImg from '@/assets/images/a-agape.png';
import logoImg from '@/assets/images/agape-logo.png';
import {
BuildingIcon,
CalendarIcon,
ClockIcon,
SpinnerIcon,
} from '@phosphor-icons/react';
import { motion } from 'framer-motion';
import * as React from 'react';
import { useSearchParams } from 'react-router-dom';
import { Navigation } from './navigation';
import ProfileCard from './ProfileCard';
import { StorySlide } from './story-slide';
export function RetrospectiveSlides() {
const [searchParams] = useSearchParams();
const cpf = searchParams.get('cpf') || '';
const ano = searchParams.get('ano')
? parseInt(searchParams.get('ano')!)
: new Date().getFullYear();
const {
data: metricsPortal,
isLoading: isLoadingPortal,
error: errorPortal,
} = useMetricsByPortal(cpf, ano);
const {
data: metricsSystems,
isLoading: isLoadingSystems,
error: errorSystems,
} = useMetricsBySystems(cpf, ano);
const [current, setCurrent] = React.useState(0);
// Calcula slides baseado nos dados disponíveis
const slides: number[] = [];
slides.push(0); // Slide inicial
if (metricsPortal && metricsPortal.length > 0) slides.push(1); // Slide portais
if (metricsSystems && metricsSystems.length > 0) slides.push(2); // Slide sistemas
if (
(metricsPortal && metricsPortal.length > 0) ||
(metricsSystems && metricsSystems.length > 0)
) {
slides.push(3); // Slide com informações gerais
}
slides.push(4); // Slide final
const total = slides.length;
const isLoading = isLoadingPortal || isLoadingSystems;
const hasError = errorPortal || errorSystems;
// Extrai total de acessos
const totalAcessos = [
...(metricsPortal || []),
...(metricsSystems || []),
].reduce((acc, item) => acc + item.totalAcessos, 0);
// Top sistemas/portais por acessos
const topItems = [...(metricsPortal || []), ...(metricsSystems || [])]
.sort((a, b) => b.totalAcessos - a.totalAcessos)
.slice(0, 5);
return (
<div className="w-full h-screen overflow-hidden">
{isLoading ? (
<div className="h-screen w-full bg-black flex items-center justify-center">
<div className="text-center space-y-4">
<SpinnerIcon className="h-12 w-12 animate-spin mx-auto text-white" />
<p className="text-white text-lg">
Carregando sua retrospectiva...
</p>
</div>
</div>
) : hasError ? (
<div className="h-screen w-full bg-black flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<p className="text-white text-lg font-semibold">
Erro ao carregar dados
</p>
<p className="text-white/70">
Verifique seu CPF e ano, e tente novamente.
</p>
</div>
</div>
) : (
<>
<Navigation total={total} current={current} setCurrent={setCurrent} />
<div className="h-full snap-y snap-mandatory overflow-y-scroll scroll-smooth">
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="text-center space-y-8">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<img
src={logoImg}
alt="Ágape Logo"
className="mx-auto w-52 mb-6"
/>
</motion.div>
<motion.h1
className="text-6xl md:text-7xl font-medium mb-4"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
Sua Retrospectiva
<br />
Ágape {ano}
</motion.h1>
<motion.p
className="text-xl md:text-2xl text-white/90 max-w-2xl mx-auto font-light"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}>
Um ano de conquistas, eficiência e transparência na gestão
pública
</motion.p>
<motion.p
className="text-lg text-white/70"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}>
Seus dados analisados: CPF
</motion.p>
<motion.div
className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.8 }}>
<span>Transparência</span>
<span></span>
<span>Eficiência</span>
<span></span>
<span>Parceria</span>
</motion.div>
</div>
</StorySlide>
{metricsPortal && metricsPortal.length > 0 && (
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="space-y-8">
<div className="text-center space-y-4">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<BuildingIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Seus Portais
</h2>
<motion.div
className="text-7xl md:text-8xl font-bold text-[#145190]"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{metricsPortal.length}
</motion.div>
<p className="text-2xl text-muted-foreground">
portais acessados neste ano
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-12">
{metricsPortal.map((portal, index) => (
<motion.div
key={index}
className="p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<h3 className="text-xl font-semibold text-[#0e233d] truncate">
{portal.nomeSistema}
</h3>
<p className="text-sm text-muted-foreground mt-1">
Acessos
</p>
<p className="text-3xl font-bold text-[#145190] mt-2">
{portal.totalAcessos.toLocaleString('pt-BR')}
</p>
</motion.div>
))}
</div>
</div>
</StorySlide>
)}
{metricsSystems && metricsSystems.length > 0 && (
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="space-y-8">
<div className="text-center space-y-4">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<CalendarIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Seus Sistemas
</h2>
<motion.div
className="text-7xl md:text-8xl font-bold text-[#145190]"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{metricsSystems.length}
</motion.div>
<p className="text-2xl text-muted-foreground">
sistemas utilizados neste ano
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-12">
{metricsSystems.map((sistema, index) => (
<motion.div
key={index}
className="p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<h3 className="text-xl font-semibold text-[#0e233d] truncate">
{sistema.nomeSistema}
</h3>
<p className="text-sm text-muted-foreground mt-1">
Acessos
</p>
<p className="text-3xl font-bold text-[#145190] mt-2">
{sistema.totalAcessos.toLocaleString('pt-BR')}
</p>
</motion.div>
))}
</div>
</div>
</StorySlide>
)}
{topItems.length > 0 && (
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center min-h-screen p-8">
<motion.div
className="space-y-8 text-center lg:text-left"
initial={{ x: -50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<ClockIcon className="h-24 w-24 mb-8 text-yellow-300 lg:mx-0" />
</motion.div>
<div className="space-y-6">
<h2 className="text-4xl md:text-5xl font-bold">
Seu Engajamento
</h2>
<motion.div
className="text-6xl md:text-7xl font-bold text-yellow-300"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{totalAcessos.toLocaleString('pt-BR')}
</motion.div>
<p className="text-xl text-white/90 max-w-lg">
Total de acessos ao longo do ano
</p>
</div>
</motion.div>
<motion.div
className="space-y-4"
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
<h3 className="text-2xl font-bold text-white mb-6">
Principais Recursos
</h3>
{topItems.slice(0, 3).map((item, index) => (
<motion.div
key={index}
className="bg-white/10 backdrop-blur border-white/20 text-white p-4 rounded-lg flex items-center justify-between"
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<div>
<p className="font-semibold">{item.nomeSistema}</p>
<p className="text-sm text-white/70">
{item.totalAcessos.toLocaleString('pt-BR')} acessos
</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold text-yellow-300">
#{index + 1}
</span>
</div>
</motion.div>
))}
</motion.div>
</div>
</StorySlide>
)}
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="h-screen max-h-screen overflow-y-auto flex flex-col md:flex-row items-center justify-center px-6 gap-12">
<div className="space-y-12 w-full md:w-1/2 text-center md:text-left">
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<img src={logoAImg} className="w-10" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
A Ágape deseja a você um {ano + 1} repleto de sucesso!
</h2>
<p className="text-xl text-muted-foreground max-w-xl">
Obrigado por fazer parte desta jornada de transformação
digital na gestão pública. Sua dedicação é essencial para
construirmos um serviço público mais eficiente e
transparente.
</p>
<motion.div
className="text-sm text-muted-foreground"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}>
<p>&copy; {ano} Ágape Sistemas e Tecnologia</p>
<p className="mt-2">
O Futuro da Gestão Pública começa aqui!
</p>
</motion.div>
</div>
<div className="w-full md:w-1/2 flex justify-center">
<div className="max-w-md w-full">
<ProfileCard
name="Usuário Ágape"
title="Gestor Público"
handle="agape"
status="Online"
contactText="Contato"
avatarUrl="https://github.com/GuilhermeSantosUI.png"
showUserInfo={true}
enableTilt={true}
enableMobileTilt={false}
onContactClick={() => console.log('Contact clicked')}
/>
</div>
</div>
</div>
</StorySlide>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import { cn } from '@/app/utils';
const Input = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement>
>(({ className, type, ...props }, ref) => (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
ref={ref}
{...props}
/>
));
Input.displayName = 'Input';
export { Input };