feat: implement login slide and metrics hooks, add API token handling
parent
24cf52f6da
commit
5a8a97e9b4
310
src/App.tsx
310
src/App.tsx
|
|
@ -1,307 +1,15 @@
|
||||||
import logoAImg from '@/assets/images/a-agape.png';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import logoImg from '@/assets/images/agape-logo.png';
|
import { LoginSlide } from './views/components/login-slide';
|
||||||
import { motion } from 'framer-motion';
|
import { RetrospectiveSlides } from './views/components/retrospective-slides';
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen overflow-hidden">
|
<Routes>
|
||||||
<Retrospectiva />
|
<Route path="/" element={<LoginSlide />} />
|
||||||
</div>
|
<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 (
|
export default App;
|
||||||
<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>
|
|
||||||
© {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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,12 @@ const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
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({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>© {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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
Loading…
Reference in New Issue