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} +

+ +
+ + Ágape Logo + + + + 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 &&
} +
+
+
+
+
+ +
+
+
+ Ágape Logo +
+ + {/* Seção de Métricas */} +
+ {/* Card de Acessos */} +
+
+

23.343

+

Acessos

+
+
+ +
+
+

8

+

Aplicações

+
+
+ +
+
+
+ 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 ( +
+
{children}
+
+ ); +} 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" } }