Fix: Remove duplicate menu and apply card effect

Removed duplicate menu and applied card spotlight effect to category cards.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-23 22:35:03 +00:00
parent 5d096818d7
commit 483cd907c8
7 changed files with 678 additions and 70 deletions

234
package-lock.json generated
View File

@ -36,6 +36,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@react-three/fiber": "^8.18.0",
"@supabase/supabase-js": "^2.49.4",
"@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.56.2",
@ -58,6 +59,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"three": "^0.177.0",
"vaul": "^0.9.3",
"zod": "^3.23.8",
"zustand": "^4.4.7"
@ -2324,6 +2326,81 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@react-three/fiber": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.26.7",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"its-fine": "^1.0.6",
"react-reconciler": "^0.27.0",
"react-use-measure": "^2.1.7",
"scheduler": "^0.21.0",
"suspend-react": "^0.1.3",
"zustand": "^3.7.1"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-file-system": ">=11.0",
"expo-gl": ">=11.0",
"react": ">=18 <19",
"react-dom": ">=18 <19",
"react-native": ">=0.64",
"three": ">=0.133"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"expo-asset": {
"optional": true
},
"expo-file-system": {
"optional": true
},
"expo-gl": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/@react-three/fiber/node_modules/scheduler": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/@react-three/fiber/node_modules/zustand": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
"license": "MIT",
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"react": ">=16.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@ -3033,14 +3110,12 @@
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@ -3057,6 +3132,21 @@
"@types/react": "*"
}
},
"node_modules/@types/react-reconciler": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
"integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/webxr": {
"version": "0.5.22",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
"integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -3469,6 +3559,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -3539,6 +3649,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -4929,6 +5063,26 @@
"node": ">= 0.4"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -5073,6 +5227,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/its-fine": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
"license": "MIT",
"dependencies": {
"@types/react-reconciler": "^0.28.0"
},
"peerDependencies": {
"react": ">=18.0"
}
},
"node_modules/its-fine/node_modules/@types/react-reconciler": {
"version": "0.28.9",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@ -6297,6 +6472,31 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-reconciler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
"integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.21.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/react-reconciler/node_modules/scheduler": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-remove-scroll": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
@ -6440,6 +6640,21 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-use-measure": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13",
"react-dom": ">=16.13"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -6844,6 +7059,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/suspend-react": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=17.0"
}
},
"node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@ -6931,6 +7155,12 @@
"node": ">=0.8"
}
},
"node_modules/three": {
"version": "0.177.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.177.0.tgz",
"integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",

View File

@ -39,6 +39,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@react-three/fiber": "^8.18.0",
"@supabase/supabase-js": "^2.49.4",
"@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.56.2",
@ -61,6 +62,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"three": "^0.177.0",
"vaul": "^0.9.3",
"zod": "^3.23.8",
"zustand": "^4.4.7"

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CardSpotlight } from '@/components/ui/card-spotlight';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
import { CategorySummary } from '@/types/financialTypes';
@ -52,13 +52,13 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
};
return (
<Card className="dashboard-card h-full shadow-xl rounded-2xl animate-fade-in bg-gradient-to-br from-orange-50 via-white to-indigo-50 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900 border-0">
<CardHeader className="pb-2 bg-gradient-to-r from-indigo-50/60 to-orange-50/0 dark:from-zinc-900 dark:to-zinc-900 rounded-t-2xl">
<CardTitle className="text-lg font-bold tracking-tight text-zinc-700 dark:text-white flex items-center">
<CardSpotlight className="dashboard-card h-full animate-fade-in bg-gradient-to-br from-orange-50 via-white to-indigo-50 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900">
<div className="pb-2 bg-gradient-to-r from-indigo-50/60 to-orange-50/0 dark:from-zinc-900 dark:to-zinc-900 rounded-t-2xl -m-6 mb-4 p-6">
<div className="text-lg font-bold tracking-tight text-zinc-700 dark:text-white flex items-center relative z-20">
<span className="mr-2">🧾</span> Gastos por Categoria
</CardTitle>
</CardHeader>
<CardContent>
</div>
</div>
<div className="relative z-20">
{isLoading ? (
<div className="flex items-center justify-center h-[260px]">
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
@ -101,8 +101,8 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
</ResponsiveContainer>
</div>
)}
</CardContent>
</Card>
</div>
</CardSpotlight>
);
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CardSpotlight } from '@/components/ui/card-spotlight';
import { ArrowUp, ArrowDown, CreditCard } from 'lucide-react';
interface TransactionSummaryCardsProps {
@ -21,42 +21,42 @@ export const TransactionSummaryCards = ({
return (
<div className="grid gap-4 md:grid-cols-3">
<Card className="border-2 border-green-200 shadow-lg">
<CardHeader className="pb-2 bg-green-50 rounded-t-lg">
<CardTitle className="text-green-700 flex items-center font-bold">
<CardSpotlight className="border-2 border-green-200">
<div className="pb-2 bg-green-50 rounded-t-lg -m-6 mb-4 p-6">
<div className="text-green-700 flex items-center font-bold relative z-20">
<ArrowUp className="h-5 w-5 mr-2 text-green-600" />
Ganhos do mês
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
</div>
</div>
<div className="pt-0 relative z-20">
<p className="text-2xl font-bold text-green-600">
{formatCurrency(totalReceitas)}
</p>
</CardContent>
</Card>
</div>
</CardSpotlight>
<Card className="border-2 border-red-200 shadow-lg">
<CardHeader className="pb-2 bg-red-50 rounded-t-lg">
<CardTitle className="text-red-700 flex items-center font-bold">
<CardSpotlight className="border-2 border-red-200">
<div className="pb-2 bg-red-50 rounded-t-lg -m-6 mb-4 p-6">
<div className="text-red-700 flex items-center font-bold relative z-20">
<ArrowDown className="h-5 w-5 mr-2 text-red-600" />
Gastos do mês
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
</div>
</div>
<div className="pt-0 relative z-20">
<p className="text-2xl font-bold text-red-600">
{formatCurrency(totalDespesas)}
</p>
</CardContent>
</Card>
</div>
</CardSpotlight>
<Card className="border-2 border-blue-200 shadow-lg">
<CardHeader className="pb-2 bg-blue-50 rounded-t-lg">
<CardTitle className="text-blue-700 flex items-center font-bold">
<CardSpotlight className="border-2 border-blue-200">
<div className="pb-2 bg-blue-50 rounded-t-lg -m-6 mb-4 p-6">
<div className="text-blue-700 flex items-center font-bold relative z-20">
<CreditCard className="h-5 w-5 mr-2 text-blue-600" />
Gastos em cartões
</CardTitle>
</CardHeader>
<CardContent className="pt-4 space-y-1">
</div>
</div>
<div className="pt-0 space-y-1 relative z-20">
<p className="text-2xl font-bold text-blue-600">
{formatCurrency(totalCartoes)}
</p>
@ -64,8 +64,8 @@ export const TransactionSummaryCards = ({
<span className="text-gray-600 font-medium">Total geral:</span>
<span className="font-bold text-red-600">{formatCurrency(totalGeral)}</span>
</div>
</CardContent>
</Card>
</div>
</CardSpotlight>
</div>
);
};

View File

@ -0,0 +1,309 @@
"use client";
import { cn } from "@/lib/utils";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import React, { useMemo, useRef } from "react";
import * as THREE from "three";
export const CanvasRevealEffect = ({
animationSpeed = 0.4,
opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1],
colors = [[0, 255, 255]],
containerClassName,
dotSize,
showGradient = true,
}: {
/**
* 0.1 - slower
* 1.0 - faster
*/
animationSpeed?: number;
opacities?: number[];
colors?: number[][];
containerClassName?: string;
dotSize?: number;
showGradient?: boolean;
}) => {
return (
<div className={cn("h-full relative bg-white w-full", containerClassName)}>
<div className="h-full w-full">
<DotMatrix
colors={colors ?? [[0, 255, 255]]}
dotSize={dotSize ?? 3}
opacities={
opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1]
}
shader={`
float animation_speed_factor = ${animationSpeed.toFixed(1)};
float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
opacity *= step(intro_offset, u_time * animation_speed_factor);
opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
`}
center={["x", "y"]}
/>
</div>
{showGradient && (
<div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" />
)}
</div>
);
};
interface DotMatrixProps {
colors?: number[][];
opacities?: number[];
totalSize?: number;
dotSize?: number;
shader?: string;
center?: ("x" | "y")[];
}
const DotMatrix: React.FC<DotMatrixProps> = ({
colors = [[0, 0, 0]],
opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
totalSize = 4,
dotSize = 2,
shader = "",
center = ["x", "y"],
}) => {
const uniforms = React.useMemo(() => {
let colorsArray = [
colors[0],
colors[0],
colors[0],
colors[0],
colors[0],
colors[0],
];
if (colors.length === 2) {
colorsArray = [
colors[0],
colors[0],
colors[0],
colors[1],
colors[1],
colors[1],
];
} else if (colors.length === 3) {
colorsArray = [
colors[0],
colors[0],
colors[1],
colors[1],
colors[2],
colors[2],
];
}
return {
u_colors: {
value: colorsArray.map((color) => [
color[0] / 255,
color[1] / 255,
color[2] / 255,
]),
type: "uniform3fv",
},
u_opacities: {
value: opacities,
type: "uniform1fv",
},
u_total_size: {
value: totalSize,
type: "uniform1f",
},
u_dot_size: {
value: dotSize,
type: "uniform1f",
},
};
}, [colors, opacities, totalSize, dotSize]);
return (
<Shader
source={`
precision mediump float;
in vec2 fragCoord;
uniform float u_time;
uniform float u_opacities[10];
uniform vec3 u_colors[6];
uniform float u_total_size;
uniform float u_dot_size;
uniform vec2 u_resolution;
out vec4 fragColor;
float PHI = 1.61803398874989484820459;
float random(vec2 xy) {
return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
}
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
void main() {
vec2 st = fragCoord.xy;
${
center.includes("x")
? "st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));"
: ""
}
${
center.includes("y")
? "st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));"
: ""
}
float opacity = step(0.0, st.x);
opacity *= step(0.0, st.y);
vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
float frequency = 5.0;
float show_offset = random(st2);
float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);
opacity *= u_opacities[int(rand * 10.0)];
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
vec3 color = u_colors[int(show_offset * 6.0)];
${shader}
fragColor = vec4(color, opacity);
fragColor.rgb *= fragColor.a;
}`}
uniforms={uniforms}
maxFps={60}
/>
);
};
type Uniforms = {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
const ShaderMaterial = ({
source,
uniforms,
maxFps = 60,
}: {
source: string;
hovered?: boolean;
maxFps?: number;
uniforms: Uniforms;
}) => {
const { size } = useThree();
const ref = useRef<THREE.Mesh>();
let lastFrameTime = 0;
useFrame(({ clock }) => {
if (!ref.current) return;
const timestamp = clock.getElapsedTime();
if (timestamp - lastFrameTime < 1 / maxFps) {
return;
}
lastFrameTime = timestamp;
const material: any = ref.current.material;
const timeLocation = material.uniforms.u_time;
timeLocation.value = timestamp;
});
const getUniforms = () => {
const preparedUniforms: any = {};
for (const uniformName in uniforms) {
const uniform: any = uniforms[uniformName];
switch (uniform.type) {
case "uniform1f":
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
break;
case "uniform3f":
preparedUniforms[uniformName] = {
value: new THREE.Vector3().fromArray(uniform.value),
type: "3f",
};
break;
case "uniform1fv":
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
break;
case "uniform3fv":
preparedUniforms[uniformName] = {
value: uniform.value.map((v: number[]) =>
new THREE.Vector3().fromArray(v)
),
type: "3fv",
};
break;
case "uniform2f":
preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value),
type: "2f",
};
break;
default:
console.error(`Invalid uniform type for '${uniformName}'.`);
break;
}
}
preparedUniforms["u_time"] = { value: 0, type: "1f" };
preparedUniforms["u_resolution"] = {
value: new THREE.Vector2(size.width * 2, size.height * 2),
};
return preparedUniforms;
};
const material = useMemo(() => {
const materialObject = new THREE.ShaderMaterial({
vertexShader: `
precision mediump float;
in vec2 coordinates;
uniform vec2 u_resolution;
out vec2 fragCoord;
void main(){
float x = position.x;
float y = position.y;
gl_Position = vec4(x, y, 0.0, 1.0);
fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
fragCoord.y = u_resolution.y - fragCoord.y;
}
`,
fragmentShader: source,
uniforms: getUniforms(),
glslVersion: THREE.GLSL3,
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor,
});
return materialObject;
}, [size.width, size.height, source]);
return (
<mesh ref={ref as any}>
<planeGeometry args={[2, 2]} />
<primitive object={material} attach="material" />
</mesh>
);
};
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
return (
<Canvas className="absolute inset-0 h-full w-full">
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
</Canvas>
);
};
interface ShaderProps {
source: string;
uniforms: {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
maxFps?: number;
}

View File

@ -0,0 +1,75 @@
"use client";
import { useMotionValue, motion, useMotionTemplate } from "framer-motion";
import React, { MouseEvent as ReactMouseEvent, useState } from "react";
import { CanvasRevealEffect } from "@/components/ui/canvas-reveal-effect";
import { cn } from "@/lib/utils";
export const CardSpotlight = ({
children,
radius = 350,
color = "#262626",
className,
...props
}: {
radius?: number;
color?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
function handleMouseMove({
currentTarget,
clientX,
clientY,
}: ReactMouseEvent<HTMLDivElement>) {
let { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
const [isHovering, setIsHovering] = useState(false);
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => setIsHovering(false);
return (
<div
className={cn(
"group/spotlight p-6 rounded-xl relative border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black shadow-lg hover:shadow-xl transition-shadow duration-300",
className
)}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
<motion.div
className="pointer-events-none absolute z-0 -inset-px rounded-xl opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
style={{
backgroundColor: color,
maskImage: useMotionTemplate`
radial-gradient(
${radius}px circle at ${mouseX}px ${mouseY}px,
white,
transparent 80%
)
`,
}}
>
{isHovering && (
<CanvasRevealEffect
animationSpeed={5}
containerClassName="bg-transparent absolute inset-0 pointer-events-none"
colors={[
[59, 130, 246],
[139, 92, 246],
]}
dotSize={3}
/>
)}
</motion.div>
{children}
</div>
);
};

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import Layout from '@/components/layout/Layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CardSpotlight } from '@/components/ui/card-spotlight';
import { getCategorySummary } from '@/services/transacao';
import { CategorySummary } from '@/types/financialTypes';
import { useToast } from "@/components/ui/use-toast";
@ -100,20 +100,20 @@ const CategoriasPage = () => {
{isLoading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Card key={i} className="animate-pulse">
<CardHeader className="pb-2">
<CardSpotlight key={i} className="animate-pulse">
<div className="pb-2">
<div className="h-5 bg-muted rounded w-1/2"></div>
</CardHeader>
<CardContent>
</div>
<div>
<div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
<div className="h-2 bg-muted rounded"></div>
</CardContent>
</Card>
</div>
</CardSpotlight>
))}
</div>
) : categories.length === 0 ? (
<Card>
<CardContent className="py-10 text-center">
<CardSpotlight>
<div className="py-10 text-center">
<p className="text-muted-foreground">Nenhuma categoria encontrada</p>
<p className="mt-2 text-sm">
{tipoFiltro === 'despesa'
@ -122,23 +122,23 @@ const CategoriasPage = () => {
? 'Verifique se existem transações do tipo "receita" cadastradas'
: 'Verifique se existem transações cadastradas'}
</p>
</CardContent>
</Card>
</div>
</CardSpotlight>
) : (
<>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{categories.map((category) => (
<Card key={category.categoria}>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center">
<CardSpotlight key={category.categoria} className="relative z-10">
<div className="pb-2">
<div className="text-base flex items-center font-semibold relative z-20">
<span
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: category.color }}
/>
{category.categoria}
</CardTitle>
</CardHeader>
<CardContent>
</div>
</div>
<div className="relative z-20">
<div className="text-2xl font-bold mb-2">
{formatCurrency(category.valor)}
</div>
@ -155,16 +155,16 @@ const CategoriasPage = () => {
{(category.percentage * 100).toFixed(1)}% do total
</div>
</div>
</CardContent>
</Card>
</div>
</CardSpotlight>
))}
</div>
<Card className="mt-6">
<CardHeader>
<CardTitle>Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</CardTitle>
</CardHeader>
<CardContent>
<CardSpotlight className="mt-6">
<div>
<h3 className="text-xl font-bold mb-4 relative z-20">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
</div>
<div className="relative z-20">
<Table>
<TableHeader>
<TableRow>
@ -194,13 +194,5 @@ const CategoriasPage = () => {
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</>
)}
</div>
</Layout>
);
};
export default CategoriasPage;
</div>
</CardSpot