diff --git a/components.json b/components.json
index 743ac58..cfb6e04 100644
--- a/components.json
+++ b/components.json
@@ -18,5 +18,7 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
- "registries": {}
-}
\ No newline at end of file
+ "registries": {
+ "@react-bits": "https://reactbits.dev/r/{name}.json"
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 718f40c..37d4a45 100644
--- a/package-lock.json
+++ b/package-lock.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",
diff --git a/package.json b/package.json
index eca2406..5beb862 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/src/App.tsx b/src/App.tsx
index 05a70f3..1e40497 100644
--- a/src/App.tsx
+++ b/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 (
+
+
+
+ );
+}
+export default function Retrospectiva() {
+ const total = 6;
+ const [current, setCurrent] = useState(0);
return (
-
-
+
+
-
- {stats.map((stat) => (
-
-
-
- {stat.title}
-
- {stat.value}
-
-
-
- {stat.description}
+
+
+
+
+
+
+
+
+ Sua Retrospectiva
+
+ Ágape {summary.year}
+
+
+
+ Um ano de conquistas, eficiência e transparência na gestão pública
+
+
+
+
+
+
+
+ Transparência
+ •
+ Eficiência
+ •
+ Parceria
+
+
+
+
+
+
+
+
+
+
+
+ Você utilizou
+
+
+ {summary.numApplicationsUsed}
+
+
+ aplicações Ágape neste ano
-
-
-
- ))}
-
-
-
-
-
- Evolução do Consumo de Combustível
-
- Comparativo de preços entre os principais postos
-
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
-
- Postos com Melhor Preço
-
- Comparativo de preços entre os principais postos
-
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
-
- Quilometragem Percorrida
-
- Total de quilômetros percorridos nos últimos meses
-
-
-
-
-
-
-
- } />
- } />
-
-
-
-
-
-
-
-
-
- Status dos Veículos
-
- Distribuição dos veículos por status de operação
-
-
-
-
-
- } />
-
-
-
-
-
-
- Frota operando com 85% de disponibilidade{' '}
-
-
- Status atual da frota de veículos
+
+
+ {summary.topApplications.map((app, index) => (
+
+
+ {app.name}
+
+
+ {app.uses} usos
+
+
+ ))}
-
-
+
+
+
+
+
+
+
+
+
+
+ Principais Ações
+
+
+ As atividades que mais impactaram sua gestão
+
+
+
+
+ {summary.topActions.map((action, index) => (
+
+
+
+
+
+ {index + 1}
+
+
+ {action.action}
+
+
+
+ {action.count.toLocaleString('pt-BR')}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Seu horário preferido
+
+
+
+ {summary.favoriteHourRange}
+
+
+ É quando você mais acessa as plataformas Ágape
+
+
+
+
+
+
+
+
+ {summary.badge.title}
+
+
+
+ {summary.badge.subtitle}
+
+
+
+ {summary.badge.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A Ágape deseja a você um {summary.year + 1} repleto de sucesso!
+
+
+
+ {summary.messageFinal}
+
+
+
+
+ © {summary.year} {summary.orgName}
+
+ O Futuro da Gestão Pública começa aqui!
+
+
+
+
+
+
console.log('Contact clicked')}
+ />
+
+
+
+
);
diff --git a/src/assets/images/a-agape.png b/src/assets/images/a-agape.png
new file mode 100644
index 0000000..fad2d69
Binary files /dev/null and b/src/assets/images/a-agape.png differ
diff --git a/src/assets/images/car-keys.png b/src/assets/images/car-keys.png
deleted file mode 100644
index 37f471e..0000000
Binary files a/src/assets/images/car-keys.png and /dev/null differ
diff --git a/src/views/components/ProfileCard.css b/src/views/components/ProfileCard.css
new file mode 100644
index 0000000..03dbc32
--- /dev/null
+++ b/src/views/components/ProfileCard.css
@@ -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;
+ }
+}
diff --git a/src/views/components/ProfileCard.jsx b/src/views/components/ProfileCard.jsx
new file mode 100644
index 0000000..8497a18
--- /dev/null
+++ b/src/views/components/ProfileCard.jsx
@@ -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 = '',
+ iconUrl = '',
+ grainUrl = '',
+ 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 (
+
+ {behindGlowEnabled &&
}
+
+
+
+
+
+
+
+
+
+

+
+
+ {/* Seção de Métricas */}
+
+ {/* Card de Acessos */}
+
+
+
+
+
+
+
+ TOP 1
+
agGestor
+
+
+ Aplicação mais utilizada
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ProfileCard = React.memo(ProfileCardComponent);
+export default ProfileCard;
diff --git a/src/views/components/mock/summary.tsx b/src/views/components/mock/summary.tsx
new file mode 100644
index 0000000..16ac71a
--- /dev/null
+++ b/src/views/components/mock/summary.tsx
@@ -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%',
+};
diff --git a/src/views/components/navigation.tsx b/src/views/components/navigation.tsx
new file mode 100644
index 0000000..03e1d76
--- /dev/null
+++ b/src/views/components/navigation.tsx
@@ -0,0 +1,21 @@
+interface NavigationProps {
+ total: number;
+ current: number;
+ setCurrent: (index: number) => void;
+}
+
+export function Navigation({ total, current, setCurrent }: NavigationProps) {
+ return (
+
+ {Array.from({ length: total }).map((_, i) => (
+
+ );
+}
diff --git a/src/views/components/story-slide.tsx b/src/views/components/story-slide.tsx
new file mode 100644
index 0000000..c6816a5
--- /dev/null
+++ b/src/views/components/story-slide.tsx
@@ -0,0 +1,13 @@
+interface StorySlideProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function StorySlide({ children, className }: StorySlideProps) {
+ return (
+
+ );
+}
diff --git a/src/views/components/ui/badge.tsx b/src/views/components/ui/badge.tsx
new file mode 100644
index 0000000..fd72e57
--- /dev/null
+++ b/src/views/components/ui/badge.tsx
@@ -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 & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/views/components/ui/carousel.tsx b/src/views/components/ui/carousel.tsx
new file mode 100644
index 0000000..b6ba491
--- /dev/null
+++ b/src/views/components/ui/carousel.tsx
@@ -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
+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[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ 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) => {
+ 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 (
+
+
+ {children}
+
+
+ )
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/tsconfig.json b/tsconfig.json
index fec8c8e..f9c40e7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,6 +8,9 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
- }
+ },
+ "allowJs": true,
+ "checkJs": false,
+ "jsx": "react-jsx"
}
}