feat: add ProfileCard component with tilt effect and metrics display
- Implemented ProfileCardComponent with dynamic tilt effect based on pointer and device orientation. - Added metrics display including access counts and top applications. - Created mock data for summary statistics and application usage. - Introduced Navigation component for pagination control. - Developed StorySlide component for full-screen slide presentation. - Added Badge and Carousel components for UI enhancements.main
parent
d16767e287
commit
206119cd42
|
|
@ -18,5 +18,7 @@
|
|||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
"registries": {
|
||||
"@react-bits": "https://reactbits.dev/r/{name}.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.26",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
|
|
@ -3875,6 +3877,35 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/embla-carousel": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/embla-carousel-react": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
|
||||
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"embla-carousel": "8.6.0",
|
||||
"embla-carousel-reactive-utils": "8.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/embla-carousel-reactive-utils": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
|
||||
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"embla-carousel": "8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
|
|
@ -4277,6 +4308,33 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.26",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
||||
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -4916,6 +4974,21 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.23.6",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.26",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
|
|
@ -48,4 +50,4 @@
|
|||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
580
src/App.tsx
580
src/App.tsx
|
|
@ -1,298 +1,306 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/views/components/ui/card';
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from '@/views/components/ui/chart';
|
||||
import { Progress } from '@/views/components/ui/progress';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
XAxis,
|
||||
} from 'recharts';
|
||||
import { Header } from './views/components/header';
|
||||
import logoAImg from '@/assets/images/a-agape.png';
|
||||
import logoImg from '@/assets/images/agape-logo.png';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export const description = 'A stacked bar chart with a legend';
|
||||
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() {
|
||||
const stats = [
|
||||
{
|
||||
title: 'Média de Consumo (R$)',
|
||||
value: 'R$ 0,00',
|
||||
description: '',
|
||||
progress: 44.6,
|
||||
},
|
||||
{
|
||||
title: 'Total de KM Percorridos',
|
||||
value: '0 KM',
|
||||
description: '',
|
||||
progress: 30.6,
|
||||
},
|
||||
{
|
||||
title: 'Viagens Realizadas no Mês',
|
||||
value: '0',
|
||||
description: '',
|
||||
progress: 24.8,
|
||||
},
|
||||
];
|
||||
|
||||
const evolutionConfig = {
|
||||
price: {
|
||||
label: 'Preço',
|
||||
color: '#0e233d',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const evolutionData = [
|
||||
{ month: 'Shell', price: 186 },
|
||||
{ month: 'Ipiranga', price: 305 },
|
||||
{ month: 'BR', price: 237 },
|
||||
];
|
||||
|
||||
const bestPricesConfig = {
|
||||
price: {
|
||||
label: 'Preço',
|
||||
color: '#0e233d',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const bestPricesData = [
|
||||
{ station: 'Shell', price: 5.42 },
|
||||
{ station: 'Ipiranga', price: 5.35 },
|
||||
{ station: 'BR', price: 5.38 },
|
||||
{ station: 'Ale', price: 5.3 },
|
||||
];
|
||||
|
||||
const vehicleStatusConfig = {
|
||||
active: {
|
||||
label: 'Ativos',
|
||||
color: '#0e233d',
|
||||
},
|
||||
maintenance: {
|
||||
label: 'Manutenção',
|
||||
color: '#173b63',
|
||||
},
|
||||
inactive: {
|
||||
label: 'Inativos',
|
||||
color: '#154677',
|
||||
},
|
||||
reserved: {
|
||||
label: 'Reservados',
|
||||
color: '#145190',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const vehicleStatusData = [
|
||||
{ status: 'active', count: 45, fill: '#0e233d' },
|
||||
{ status: 'maintenance', count: 12, fill: '#173b63' },
|
||||
{ status: 'inactive', count: 8, fill: '#154677' },
|
||||
{ status: 'reserved', count: 15, fill: '#145190' },
|
||||
];
|
||||
|
||||
const mileageConfig = {
|
||||
company: {
|
||||
label: 'Frota Empresa',
|
||||
color: '#0e233d',
|
||||
},
|
||||
rented: {
|
||||
label: 'Frota Terceirizada',
|
||||
color: '#173b63',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const mileageData = [
|
||||
{ month: 'Jan', company: 1200, rented: 800 },
|
||||
{ month: 'Fev', company: 1350, rented: 750 },
|
||||
{ month: 'Mar', company: 1100, rented: 900 },
|
||||
{ month: 'Abr', company: 1400, rented: 600 },
|
||||
{ month: 'Mai', company: 1250, rented: 850 },
|
||||
{ month: 'Jun', company: 1300, rented: 700 },
|
||||
];
|
||||
return (
|
||||
<div className="w-full h-screen overflow-hidden">
|
||||
<Retrospectiva />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function Retrospectiva() {
|
||||
const total = 6;
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="p-[18px] flex flex-col gap-6">
|
||||
<Header />
|
||||
<div className="h-screen w-full overflow-hidden bg-black">
|
||||
<Navigation total={total} current={current} setCurrent={setCurrent} />
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{stats.map((stat) => (
|
||||
<Card
|
||||
key={stat.title}
|
||||
className="hover:shadow-md transition-shadow duration-300"
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{stat.title}
|
||||
</CardTitle>
|
||||
<div className="text-4xl font-bold">{stat.value}</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{stat.description}
|
||||
<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>
|
||||
<Progress value={stat.progress} className="h-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Evolução do Consumo de Combustível</CardTitle>
|
||||
<CardDescription>
|
||||
Comparativo de preços entre os principais postos
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={evolutionConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={evolutionData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Line
|
||||
dataKey="price"
|
||||
type="natural"
|
||||
stroke="var(--color-price)"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: 'var(--color-price)',
|
||||
}}
|
||||
activeDot={{
|
||||
r: 6,
|
||||
}}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Postos com Melhor Preço</CardTitle>
|
||||
<CardDescription>
|
||||
Comparativo de preços entre os principais postos
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={bestPricesConfig}>
|
||||
<BarChart accessibilityLayer data={bestPricesData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="station"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar dataKey="price" fill="var(--color-price)" radius={8} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quilometragem Percorrida</CardTitle>
|
||||
<CardDescription>
|
||||
Total de quilômetros percorridos nos últimos meses
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={mileageConfig}>
|
||||
<BarChart accessibilityLayer data={mileageData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="company"
|
||||
stackId="a"
|
||||
fill="var(--color-company)"
|
||||
radius={[0, 0, 4, 4]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="rented"
|
||||
stackId="a"
|
||||
fill="var(--color-rented)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Status dos Veículos</CardTitle>
|
||||
<CardDescription>
|
||||
Distribuição dos veículos por status de operação
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer
|
||||
config={vehicleStatusConfig}
|
||||
className="[&_.recharts-pie-label-text]:fill-foreground mx-auto aspect-square max-h-[320px] pb-0"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={vehicleStatusData}
|
||||
dataKey="count"
|
||||
label
|
||||
nameKey="status"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 leading-none font-medium">
|
||||
Frota operando com 85% de disponibilidade{' '}
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground leading-none">
|
||||
Status atual da frota de veículos
|
||||
|
||||
<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>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB |
|
|
@ -0,0 +1,726 @@
|
|||
:root {
|
||||
--pointer-x: 50%;
|
||||
--pointer-y: 50%;
|
||||
--pointer-from-center: 0;
|
||||
--pointer-from-top: 0.5;
|
||||
--pointer-from-left: 0.5;
|
||||
--card-opacity: 0;
|
||||
--rotate-x: 0deg;
|
||||
--rotate-y: 0deg;
|
||||
--background-x: 50%;
|
||||
--background-y: 50%;
|
||||
--grain: none;
|
||||
--icon: none;
|
||||
--behind-gradient: none;
|
||||
--behind-glow-color: rgba(125, 190, 255, 0.67);
|
||||
--behind-glow-size: 25%;
|
||||
--inner-gradient: none;
|
||||
--sunpillar-1: hsl(2, 100%, 73%);
|
||||
--sunpillar-2: hsl(53, 100%, 69%);
|
||||
--sunpillar-3: hsl(93, 100%, 69%);
|
||||
--sunpillar-4: hsl(176, 100%, 76%);
|
||||
--sunpillar-5: hsl(228, 100%, 74%);
|
||||
--sunpillar-6: hsl(283, 100%, 73%);
|
||||
--sunpillar-clr-1: var(--sunpillar-1);
|
||||
--sunpillar-clr-2: var(--sunpillar-2);
|
||||
--sunpillar-clr-3: var(--sunpillar-3);
|
||||
--sunpillar-clr-4: var(--sunpillar-4);
|
||||
--sunpillar-clr-5: var(--sunpillar-5);
|
||||
--sunpillar-clr-6: var(--sunpillar-6);
|
||||
--card-radius: 30px;
|
||||
}
|
||||
|
||||
.pc-card-wrapper {
|
||||
perspective: 500px;
|
||||
transform: translate3d(0, 0, 0.1px);
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.pc-behind {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
circle at var(--pointer-x) var(--pointer-y),
|
||||
var(--behind-glow-color) 0%,
|
||||
transparent var(--behind-glow-size)
|
||||
);
|
||||
filter: blur(50px) saturate(1.1);
|
||||
opacity: calc(0.8 * var(--card-opacity));
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.pc-card-wrapper:hover,
|
||||
.pc-card-wrapper.active {
|
||||
--card-opacity: 1;
|
||||
}
|
||||
|
||||
.pc-card {
|
||||
height: 80svh;
|
||||
max-height: 540px;
|
||||
display: grid;
|
||||
aspect-ratio: 0.718;
|
||||
border-radius: var(--card-radius);
|
||||
position: relative;
|
||||
background-blend-mode: color-dodge, normal, normal, normal;
|
||||
animation: glow-bg 12s linear infinite;
|
||||
box-shadow: rgba(0, 0, 0, 0.8) calc((var(--pointer-from-left) * 10px) - 3px)
|
||||
calc((var(--pointer-from-top) * 20px) - 6px) 20px -5px;
|
||||
transition: transform 1s ease;
|
||||
transform: translateZ(0) rotateX(0deg) rotateY(0deg);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backface-visibility: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pc-card:hover,
|
||||
.pc-card.active {
|
||||
transition: none;
|
||||
transform: translateZ(0) rotateX(var(--rotate-y)) rotateY(var(--rotate-x));
|
||||
}
|
||||
|
||||
.pc-card-shell.entering .pc-card {
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.pc-card-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pc-card * {
|
||||
display: grid;
|
||||
grid-area: 1/-1;
|
||||
border-radius: var(--card-radius);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pc-inside {
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
background-image: var(--inner-gradient);
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.pc-shine {
|
||||
mask-image: var(--icon);
|
||||
mask-mode: luminance;
|
||||
mask-repeat: repeat;
|
||||
mask-size: 150%;
|
||||
mask-position: top calc(200% - (var(--background-y) * 5)) left
|
||||
calc(100% - var(--background-x));
|
||||
transition: filter 0.8s ease;
|
||||
filter: brightness(0.66) contrast(1.33) saturate(0.33) opacity(0.5);
|
||||
animation: holo-bg 18s linear infinite;
|
||||
animation-play-state: running;
|
||||
mix-blend-mode: color-dodge;
|
||||
}
|
||||
|
||||
.pc-shine,
|
||||
.pc-shine::after {
|
||||
--space: 5%;
|
||||
--angle: -45deg;
|
||||
transform: translate3d(0, 0, 1px);
|
||||
overflow: hidden;
|
||||
z-index: 3;
|
||||
background: transparent;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
var(--sunpillar-clr-1) calc(var(--space) * 1),
|
||||
var(--sunpillar-clr-2) calc(var(--space) * 2),
|
||||
var(--sunpillar-clr-3) calc(var(--space) * 3),
|
||||
var(--sunpillar-clr-4) calc(var(--space) * 4),
|
||||
var(--sunpillar-clr-5) calc(var(--space) * 5),
|
||||
var(--sunpillar-clr-6) calc(var(--space) * 6),
|
||||
var(--sunpillar-clr-1) calc(var(--space) * 7)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
var(--angle),
|
||||
#0e152e 0%,
|
||||
hsl(180, 10%, 60%) 3.8%,
|
||||
hsl(180, 29%, 66%) 4.5%,
|
||||
hsl(180, 10%, 60%) 5.2%,
|
||||
#0e152e 10%,
|
||||
#0e152e 12%
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-corner circle at var(--pointer-x) var(--pointer-y),
|
||||
hsla(0, 0%, 0%, 0.1) 12%,
|
||||
hsla(0, 0%, 0%, 0.15) 20%,
|
||||
hsla(0, 0%, 0%, 0.25) 120%
|
||||
);
|
||||
background-position: 0 var(--background-y),
|
||||
var(--background-x) var(--background-y), center;
|
||||
background-blend-mode: color, hard-light;
|
||||
background-size: 500% 500%, 300% 300%, 200% 200%;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.pc-shine::before,
|
||||
.pc-shine::after {
|
||||
content: '';
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
grid-area: 1/1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease;
|
||||
}
|
||||
|
||||
.pc-card:hover .pc-shine,
|
||||
.pc-card.active .pc-shine {
|
||||
filter: brightness(0.85) contrast(1.5) saturate(0.5);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.pc-card:hover .pc-shine::before,
|
||||
.pc-card.active .pc-shine::before,
|
||||
.pc-card:hover .pc-shine::after,
|
||||
.pc-card.active .pc-shine::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pc-shine::before {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--sunpillar-4),
|
||||
var(--sunpillar-5),
|
||||
var(--sunpillar-6),
|
||||
var(--sunpillar-1),
|
||||
var(--sunpillar-2),
|
||||
var(--sunpillar-3)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--pointer-x) var(--pointer-y),
|
||||
hsl(0, 0%, 70%) 0%,
|
||||
hsla(0, 0%, 30%, 0.2) 90%
|
||||
),
|
||||
var(--grain);
|
||||
background-size: 250% 250%, 100% 100%, 220px 220px;
|
||||
background-position: var(--pointer-x) var(--pointer-y), center,
|
||||
calc(var(--pointer-x) * 0.01) calc(var(--pointer-y) * 0.01);
|
||||
background-blend-mode: color-dodge;
|
||||
filter: brightness(calc(2 - var(--pointer-from-center)))
|
||||
contrast(calc(var(--pointer-from-center) + 2))
|
||||
saturate(calc(0.5 + var(--pointer-from-center)));
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.pc-shine::after {
|
||||
background-position: 0 var(--background-y),
|
||||
calc(var(--background-x) * 0.4) calc(var(--background-y) * 0.5), center;
|
||||
background-size: 200% 300%, 700% 700%, 100% 100%;
|
||||
mix-blend-mode: difference;
|
||||
filter: brightness(0.8) contrast(1.5);
|
||||
}
|
||||
|
||||
.pc-glare {
|
||||
transform: translate3d(0, 0, 1.1px);
|
||||
overflow: hidden;
|
||||
background-image: radial-gradient(
|
||||
farthest-corner circle at var(--pointer-x) var(--pointer-y),
|
||||
hsl(248, 25%, 80%) 12%,
|
||||
hsla(207, 40%, 30%, 0.8) 90%
|
||||
);
|
||||
mix-blend-mode: overlay;
|
||||
filter: brightness(0.8) contrast(1.2);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.pc-avatar-content {
|
||||
mix-blend-mode: luminosity;
|
||||
overflow: visible;
|
||||
transform: translateZ(2);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.pc-avatar-content .avatar {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform-origin: 50% 100%;
|
||||
transform: translateX(calc(-50% + (var(--pointer-from-left) - 0.5) * 6px))
|
||||
translateZ(0) scaleY(calc(1 + (var(--pointer-from-top) - 0.5) * 0.02))
|
||||
scaleX(calc(1 + (var(--pointer-from-left) - 0.5) * 0.01));
|
||||
bottom: -1px;
|
||||
backface-visibility: hidden;
|
||||
will-change: transform;
|
||||
transition: transform 120ms ease-out;
|
||||
}
|
||||
|
||||
.pc-avatar-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
backdrop-filter: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pc-user-info {
|
||||
position: absolute;
|
||||
--ui-inset: 20px;
|
||||
--ui-radius-bias: 6px;
|
||||
bottom: var(--ui-inset);
|
||||
left: var(--ui-inset);
|
||||
right: var(--ui-inset);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(30px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: calc(
|
||||
max(0px, var(--card-radius) - var(--ui-inset) + var(--ui-radius-bias))
|
||||
);
|
||||
padding: 12px 14px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.pc-user-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pc-mini-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pc-mini-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pc-user-text {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pc-handle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pc-status {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pc-contact-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.pc-contact-btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pc-content:not(.pc-avatar-content) {
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transform: translate3d(
|
||||
calc(var(--pointer-from-left) * -6px + 3px),
|
||||
calc(var(--pointer-from-top) * -6px + 3px),
|
||||
0.1px
|
||||
);
|
||||
z-index: 5;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.pc-details {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 3em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pc-details h3 {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
font-size: min(5svh, 3em);
|
||||
margin: 0;
|
||||
background-image: linear-gradient(to bottom, #fff, #6f6fbe);
|
||||
background-size: 1em 1.5em;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.pc-details p {
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
white-space: nowrap;
|
||||
font-size: 16px;
|
||||
margin: 0 auto;
|
||||
width: min-content;
|
||||
background-image: linear-gradient(to bottom, #fff, #4a4ac0);
|
||||
background-size: 1em 1.5em;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
@keyframes glow-bg {
|
||||
0% {
|
||||
--bgrotate: 0deg;
|
||||
}
|
||||
|
||||
100% {
|
||||
--bgrotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes holo-bg {
|
||||
0% {
|
||||
background-position: 0 var(--background-y), 0 0, center;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 var(--background-y), 90% 90%, center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pc-card {
|
||||
height: 70svh;
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
.pc-details {
|
||||
top: 2em;
|
||||
}
|
||||
|
||||
.pc-details h3 {
|
||||
font-size: min(4svh, 2.5em);
|
||||
}
|
||||
|
||||
.pc-details p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pc-user-info {
|
||||
--ui-inset: 15px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.pc-mini-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.pc-user-details {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pc-handle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pc-status {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.pc-contact-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pc-card {
|
||||
height: 60svh;
|
||||
max-height: 380px;
|
||||
}
|
||||
|
||||
.pc-details {
|
||||
top: 1.5em;
|
||||
}
|
||||
|
||||
.pc-details h3 {
|
||||
font-size: min(3.5svh, 2em);
|
||||
}
|
||||
|
||||
.pc-details p {
|
||||
font-size: 12px;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
.pc-user-info {
|
||||
--ui-inset: 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.pc-mini-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.pc-user-details {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pc-handle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pc-status {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.pc-contact-btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 10px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.pc-card {
|
||||
height: 55svh;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.pc-details h3 {
|
||||
font-size: min(3svh, 1.5em);
|
||||
}
|
||||
|
||||
.pc-details p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pc-user-info {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.pc-mini-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.pc-user-details {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pc-handle {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pc-status {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.pc-contact-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 9px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilos para as métricas */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metric-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-card-wide {
|
||||
grid-column: span 2;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metric-card-wide::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.metric-card-wide:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.metric-icon-wide {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-content-wide {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.metric-app-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-app-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 480px) {
|
||||
.metrics-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.metric-card-wide {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.metric-app-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import './ProfileCard.css';
|
||||
|
||||
import logoImg from '@/assets/images/agape-logo.png';
|
||||
|
||||
const DEFAULT_INNER_GRADIENT =
|
||||
'linear-gradient(145deg,#60496e8c 0%,#71C4FF44 100%)';
|
||||
|
||||
const ANIMATION_CONFIG = {
|
||||
INITIAL_DURATION: 1200,
|
||||
INITIAL_X_OFFSET: 70,
|
||||
INITIAL_Y_OFFSET: 60,
|
||||
DEVICE_BETA_OFFSET: 20,
|
||||
ENTER_TRANSITION_MS: 180,
|
||||
};
|
||||
|
||||
const clamp = (v, min = 0, max = 100) => Math.min(Math.max(v, min), max);
|
||||
const round = (v, precision = 3) => parseFloat(v.toFixed(precision));
|
||||
const adjust = (v, fMin, fMax, tMin, tMax) =>
|
||||
round(tMin + ((tMax - tMin) * (v - fMin)) / (fMax - fMin));
|
||||
|
||||
const ProfileCardComponent = ({
|
||||
avatarUrl = '<Placeholder for avatar URL>',
|
||||
iconUrl = '<Placeholder for icon URL>',
|
||||
grainUrl = '<Placeholder for grain URL>',
|
||||
innerGradient,
|
||||
behindGlowEnabled = true,
|
||||
behindGlowColor,
|
||||
behindGlowSize,
|
||||
className = '',
|
||||
enableTilt = true,
|
||||
enableMobileTilt = false,
|
||||
mobileTiltSensitivity = 5,
|
||||
miniAvatarUrl,
|
||||
name = 'Javi A. Torres',
|
||||
title = 'Software Engineer',
|
||||
handle = 'javicodes',
|
||||
status = 'Online',
|
||||
contactText = 'Contact',
|
||||
showUserInfo = true,
|
||||
onContactClick,
|
||||
}) => {
|
||||
const wrapRef = useRef(null);
|
||||
const shellRef = useRef(null);
|
||||
|
||||
const enterTimerRef = useRef(null);
|
||||
const leaveRafRef = useRef(null);
|
||||
|
||||
const tiltEngine = useMemo(() => {
|
||||
if (!enableTilt) return null;
|
||||
|
||||
let rafId = null;
|
||||
let running = false;
|
||||
let lastTs = 0;
|
||||
|
||||
let currentX = 0;
|
||||
let currentY = 0;
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
|
||||
const DEFAULT_TAU = 0.14;
|
||||
const INITIAL_TAU = 0.6;
|
||||
let initialUntil = 0;
|
||||
|
||||
const setVarsFromXY = (x, y) => {
|
||||
const shell = shellRef.current;
|
||||
const wrap = wrapRef.current;
|
||||
if (!shell || !wrap) return;
|
||||
|
||||
const width = shell.clientWidth || 1;
|
||||
const height = shell.clientHeight || 1;
|
||||
|
||||
const percentX = clamp((100 / width) * x);
|
||||
const percentY = clamp((100 / height) * y);
|
||||
|
||||
const centerX = percentX - 50;
|
||||
const centerY = percentY - 50;
|
||||
|
||||
const properties = {
|
||||
'--pointer-x': `${percentX}%`,
|
||||
'--pointer-y': `${percentY}%`,
|
||||
'--background-x': `${adjust(percentX, 0, 100, 35, 65)}%`,
|
||||
'--background-y': `${adjust(percentY, 0, 100, 35, 65)}%`,
|
||||
'--pointer-from-center': `${clamp(
|
||||
Math.hypot(percentY - 50, percentX - 50) / 50,
|
||||
0,
|
||||
1,
|
||||
)}`,
|
||||
'--pointer-from-top': `${percentY / 100}`,
|
||||
'--pointer-from-left': `${percentX / 100}`,
|
||||
'--rotate-x': `${round(-(centerX / 5))}deg`,
|
||||
'--rotate-y': `${round(centerY / 4)}deg`,
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(properties))
|
||||
wrap.style.setProperty(k, v);
|
||||
};
|
||||
|
||||
const step = (ts) => {
|
||||
if (!running) return;
|
||||
if (lastTs === 0) lastTs = ts;
|
||||
const dt = (ts - lastTs) / 1000;
|
||||
lastTs = ts;
|
||||
|
||||
const tau = ts < initialUntil ? INITIAL_TAU : DEFAULT_TAU;
|
||||
const k = 1 - Math.exp(-dt / tau);
|
||||
|
||||
currentX += (targetX - currentX) * k;
|
||||
currentY += (targetY - currentY) * k;
|
||||
|
||||
setVarsFromXY(currentX, currentY);
|
||||
|
||||
const stillFar =
|
||||
Math.abs(targetX - currentX) > 0.05 ||
|
||||
Math.abs(targetY - currentY) > 0.05;
|
||||
|
||||
if (stillFar || document.hasFocus()) {
|
||||
rafId = requestAnimationFrame(step);
|
||||
} else {
|
||||
running = false;
|
||||
lastTs = 0;
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (running) return;
|
||||
running = true;
|
||||
lastTs = 0;
|
||||
rafId = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
return {
|
||||
setImmediate(x, y) {
|
||||
currentX = x;
|
||||
currentY = y;
|
||||
setVarsFromXY(currentX, currentY);
|
||||
},
|
||||
setTarget(x, y) {
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
start();
|
||||
},
|
||||
toCenter() {
|
||||
const shell = shellRef.current;
|
||||
if (!shell) return;
|
||||
this.setTarget(shell.clientWidth / 2, shell.clientHeight / 2);
|
||||
},
|
||||
beginInitial(durationMs) {
|
||||
initialUntil = performance.now() + durationMs;
|
||||
start();
|
||||
},
|
||||
getCurrent() {
|
||||
return { x: currentX, y: currentY, tx: targetX, ty: targetY };
|
||||
},
|
||||
cancel() {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
running = false;
|
||||
lastTs = 0;
|
||||
},
|
||||
};
|
||||
}, [enableTilt]);
|
||||
|
||||
const getOffsets = (evt, el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
|
||||
};
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event) => {
|
||||
const shell = shellRef.current;
|
||||
if (!shell || !tiltEngine) return;
|
||||
const { x, y } = getOffsets(event, shell);
|
||||
tiltEngine.setTarget(x, y);
|
||||
},
|
||||
[tiltEngine],
|
||||
);
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(event) => {
|
||||
const shell = shellRef.current;
|
||||
if (!shell || !tiltEngine) return;
|
||||
|
||||
shell.classList.add('active');
|
||||
shell.classList.add('entering');
|
||||
if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);
|
||||
enterTimerRef.current = window.setTimeout(() => {
|
||||
shell.classList.remove('entering');
|
||||
}, ANIMATION_CONFIG.ENTER_TRANSITION_MS);
|
||||
|
||||
const { x, y } = getOffsets(event, shell);
|
||||
tiltEngine.setTarget(x, y);
|
||||
},
|
||||
[tiltEngine],
|
||||
);
|
||||
|
||||
const handlePointerLeave = useCallback(() => {
|
||||
const shell = shellRef.current;
|
||||
if (!shell || !tiltEngine) return;
|
||||
|
||||
tiltEngine.toCenter();
|
||||
|
||||
const checkSettle = () => {
|
||||
const { x, y, tx, ty } = tiltEngine.getCurrent();
|
||||
const settled = Math.hypot(tx - x, ty - y) < 0.6;
|
||||
if (settled) {
|
||||
shell.classList.remove('active');
|
||||
leaveRafRef.current = null;
|
||||
} else {
|
||||
leaveRafRef.current = requestAnimationFrame(checkSettle);
|
||||
}
|
||||
};
|
||||
if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);
|
||||
leaveRafRef.current = requestAnimationFrame(checkSettle);
|
||||
}, [tiltEngine]);
|
||||
|
||||
const handleDeviceOrientation = useCallback(
|
||||
(event) => {
|
||||
const shell = shellRef.current;
|
||||
if (!shell || !tiltEngine) return;
|
||||
|
||||
const { beta, gamma } = event;
|
||||
if (beta == null || gamma == null) return;
|
||||
|
||||
const centerX = shell.clientWidth / 2;
|
||||
const centerY = shell.clientHeight / 2;
|
||||
const x = clamp(
|
||||
centerX + gamma * mobileTiltSensitivity,
|
||||
0,
|
||||
shell.clientWidth,
|
||||
);
|
||||
const y = clamp(
|
||||
centerY +
|
||||
(beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,
|
||||
0,
|
||||
shell.clientHeight,
|
||||
);
|
||||
|
||||
tiltEngine.setTarget(x, y);
|
||||
},
|
||||
[tiltEngine, mobileTiltSensitivity],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTilt || !tiltEngine) return;
|
||||
|
||||
const shell = shellRef.current;
|
||||
if (!shell) return;
|
||||
|
||||
const pointerMoveHandler = handlePointerMove;
|
||||
const pointerEnterHandler = handlePointerEnter;
|
||||
const pointerLeaveHandler = handlePointerLeave;
|
||||
const deviceOrientationHandler = handleDeviceOrientation;
|
||||
|
||||
shell.addEventListener('pointerenter', pointerEnterHandler);
|
||||
shell.addEventListener('pointermove', pointerMoveHandler);
|
||||
shell.addEventListener('pointerleave', pointerLeaveHandler);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!enableMobileTilt || location.protocol !== 'https:') return;
|
||||
const anyMotion = window.DeviceMotionEvent;
|
||||
if (anyMotion && typeof anyMotion.requestPermission === 'function') {
|
||||
anyMotion
|
||||
.requestPermission()
|
||||
.then((state) => {
|
||||
if (state === 'granted') {
|
||||
window.addEventListener(
|
||||
'deviceorientation',
|
||||
deviceOrientationHandler,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
window.addEventListener('deviceorientation', deviceOrientationHandler);
|
||||
}
|
||||
};
|
||||
shell.addEventListener('click', handleClick);
|
||||
|
||||
const initialX =
|
||||
(shell.clientWidth || 0) - ANIMATION_CONFIG.INITIAL_X_OFFSET;
|
||||
const initialY = ANIMATION_CONFIG.INITIAL_Y_OFFSET;
|
||||
tiltEngine.setImmediate(initialX, initialY);
|
||||
tiltEngine.toCenter();
|
||||
tiltEngine.beginInitial(ANIMATION_CONFIG.INITIAL_DURATION);
|
||||
|
||||
return () => {
|
||||
shell.removeEventListener('pointerenter', pointerEnterHandler);
|
||||
shell.removeEventListener('pointermove', pointerMoveHandler);
|
||||
shell.removeEventListener('pointerleave', pointerLeaveHandler);
|
||||
shell.removeEventListener('click', handleClick);
|
||||
window.removeEventListener('deviceorientation', deviceOrientationHandler);
|
||||
if (enterTimerRef.current) window.clearTimeout(enterTimerRef.current);
|
||||
if (leaveRafRef.current) cancelAnimationFrame(leaveRafRef.current);
|
||||
tiltEngine.cancel();
|
||||
shell.classList.remove('entering');
|
||||
};
|
||||
}, [
|
||||
enableTilt,
|
||||
enableMobileTilt,
|
||||
tiltEngine,
|
||||
handlePointerMove,
|
||||
handlePointerEnter,
|
||||
handlePointerLeave,
|
||||
handleDeviceOrientation,
|
||||
]);
|
||||
|
||||
const cardStyle = useMemo(
|
||||
() => ({
|
||||
'--icon': iconUrl ? `url(${iconUrl})` : 'none',
|
||||
'--grain': grainUrl ? `url(${grainUrl})` : 'none',
|
||||
'--inner-gradient': innerGradient ?? DEFAULT_INNER_GRADIENT,
|
||||
'--behind-glow-color': behindGlowColor ?? 'rgba(125, 190, 255, 0.67)',
|
||||
'--behind-glow-size': behindGlowSize ?? '50%',
|
||||
}),
|
||||
[iconUrl, grainUrl, innerGradient, behindGlowColor, behindGlowSize],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapRef}
|
||||
className={`pc-card-wrapper ${className}`.trim()}
|
||||
style={cardStyle}>
|
||||
{behindGlowEnabled && <div className="pc-behind" />}
|
||||
<div ref={shellRef} className="pc-card-shell">
|
||||
<section className="pc-card">
|
||||
<div className="pc-inside">
|
||||
<div className="pc-shine" />
|
||||
<div className="pc-glare" />
|
||||
|
||||
<div className="pc-content">
|
||||
<div className="pc-details">
|
||||
<div className="w-full flex items-center justify-center mb-8">
|
||||
<img src={logoImg} className="w-44" alt="Ágape Logo" />
|
||||
</div>
|
||||
|
||||
{/* Seção de Métricas */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
padding: '24px',
|
||||
}}>
|
||||
{/* Card de Acessos */}
|
||||
<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>
|
||||
<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>
|
||||
<p className="metric-label">Aplicações</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card-wide bg-gradient-to-br from-indigo-900/30 to-purple-900/30">
|
||||
<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>
|
||||
</div>
|
||||
<p className="metric-app-desc">
|
||||
Aplicação mais utilizada
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileCard = React.memo(ProfileCardComponent);
|
||||
export default ProfileCard;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
export const summary = {
|
||||
year: 2025,
|
||||
orgName: 'Ágape Sistemas e Tecnologia',
|
||||
numApplicationsUsed: 42,
|
||||
messageFinal:
|
||||
'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.',
|
||||
topApplications: [
|
||||
{
|
||||
name: 'Portal do Servidor',
|
||||
uses: 156,
|
||||
},
|
||||
{
|
||||
name: 'Sistema de Protocolo',
|
||||
uses: 89,
|
||||
},
|
||||
{
|
||||
name: 'Gestão de Processos',
|
||||
uses: 67,
|
||||
},
|
||||
{
|
||||
name: 'Controle de Ponto',
|
||||
uses: 54,
|
||||
},
|
||||
{
|
||||
name: 'Licitações Online',
|
||||
uses: 42,
|
||||
},
|
||||
],
|
||||
|
||||
distributionByDepartment: [
|
||||
{
|
||||
name: 'Saúde',
|
||||
value: 28,
|
||||
},
|
||||
{
|
||||
name: 'Educação',
|
||||
value: 22,
|
||||
},
|
||||
{
|
||||
name: 'Administração',
|
||||
value: 18,
|
||||
},
|
||||
{
|
||||
name: 'Finanças',
|
||||
value: 16,
|
||||
},
|
||||
{
|
||||
name: 'Infraestrutura',
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: 'Outros',
|
||||
value: 4,
|
||||
},
|
||||
],
|
||||
|
||||
topActions: [
|
||||
{
|
||||
action: 'Protocolo de Documentos',
|
||||
count: 156,
|
||||
},
|
||||
{
|
||||
action: 'Análise de Processos',
|
||||
count: 134,
|
||||
},
|
||||
{
|
||||
action: 'Solicitação de Aquisições',
|
||||
count: 98,
|
||||
},
|
||||
{
|
||||
action: 'Controle de Ponto',
|
||||
count: 87,
|
||||
},
|
||||
{
|
||||
action: 'Gestão de Licitações',
|
||||
count: 76,
|
||||
},
|
||||
{
|
||||
action: 'Relatórios Gerenciais',
|
||||
count: 65,
|
||||
},
|
||||
],
|
||||
|
||||
favoriteHourRange: '09:00 - 11:00',
|
||||
|
||||
badge: {
|
||||
title: 'Agente da Eficiência',
|
||||
subtitle: 'Nível Ouro',
|
||||
description:
|
||||
'Você se destacou pela consistência e otimização no uso das ferramentas Ágape, contribuindo significativamente para a melhoria dos processos da gestão pública.',
|
||||
},
|
||||
|
||||
// Opcional: Estatísticas adicionais
|
||||
totalDocumentsProcessed: 2456,
|
||||
averageProcessingTime: '2.3 dias',
|
||||
efficiencyGain: '37%',
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
interface NavigationProps {
|
||||
total: number;
|
||||
current: number;
|
||||
setCurrent: (index: number) => void;
|
||||
}
|
||||
|
||||
export function Navigation({ total, current, setCurrent }: NavigationProps) {
|
||||
return (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 flex gap-2 z-50">
|
||||
{Array.from({ length: total }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrent(i)}
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
current === i ? 'bg-white w-8' : 'bg-white/40 w-4'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
interface StorySlideProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StorySlide({ children, className }: StorySlideProps) {
|
||||
return (
|
||||
<section
|
||||
className={`h-screen w-full snap-start flex items-center justify-center p-8 ${className}`}>
|
||||
<div className="w-full max-w-5xl mx-auto">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/app/utils"
|
||||
import { Button } from "@/views/components/ui/button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue