Skip to content

Commit bd0e17e

Browse files
Improved Plugin Manager Cards to Match Dashboard Card Style (#2296)
* Improve Plugin Manager Cards to Match Dashboard Card Style feat: add StatCard component for plugin statistics display in PluginManager Fixes #2268 * feat: enhance StatCard component with gradient color functions and optimize PluginManager stats calculation Signed-off-by: Abhishek-Punhani <[email protected]> --------- Signed-off-by: Abhishek-Punhani <[email protected]>
1 parent a04bd64 commit bd0e17e

File tree

3 files changed

+247
-239
lines changed

3 files changed

+247
-239
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import React from 'react';
2+
import { motion, Variants } from 'framer-motion';
3+
import { Link } from 'react-router-dom';
4+
import { ChevronUp, ChevronDown, FileText, Activity } from 'lucide-react';
5+
6+
interface StatCardProps {
7+
title: string;
8+
value: number | string;
9+
icon: React.ElementType;
10+
change?: number;
11+
iconColor: 'blue' | 'green' | 'purple' | 'amber' | 'gray';
12+
isContext?: boolean;
13+
link?: string;
14+
variants?: Variants;
15+
}
16+
17+
const defaultVariants: Variants = {
18+
initial: { opacity: 0, y: 20 },
19+
animate: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } },
20+
exit: { opacity: 0, y: -10, transition: { duration: 0.3 } },
21+
};
22+
23+
// Get colors for gradient based on the icon color type
24+
const getGradient = (iconColor: string) => {
25+
switch (iconColor) {
26+
case 'blue':
27+
return 'bg-gradient-to-br from-blue-500/10 to-indigo-600/5 dark:from-blue-900/20 dark:to-indigo-900/10';
28+
case 'green':
29+
return 'bg-gradient-to-br from-emerald-500/10 to-green-600/5 dark:from-emerald-900/20 dark:to-green-900/10';
30+
case 'purple':
31+
return 'bg-gradient-to-br from-violet-500/10 to-purple-600/5 dark:from-violet-900/20 dark:to-purple-900/10';
32+
case 'amber':
33+
return 'bg-gradient-to-br from-amber-500/10 to-orange-600/5 dark:from-amber-900/20 dark:to-orange-900/10';
34+
case 'gray':
35+
default:
36+
return 'bg-gradient-to-br from-gray-600/15 to-gray-700/10 dark:from-gray-800/30 dark:to-gray-900/20';
37+
}
38+
};
39+
40+
// Get colors for the icon container
41+
const getIconGradient = (iconColor: string) => {
42+
switch (iconColor) {
43+
case 'blue':
44+
return 'bg-gradient-to-br from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500';
45+
case 'green':
46+
return 'bg-gradient-to-br from-emerald-500 to-green-600 dark:from-emerald-400 dark:to-green-500';
47+
case 'purple':
48+
return 'bg-gradient-to-br from-violet-500 to-purple-600 dark:from-violet-400 dark:to-purple-500';
49+
case 'amber':
50+
return 'bg-gradient-to-br from-amber-500 to-orange-600 dark:from-amber-400 dark:to-orange-500';
51+
case 'gray':
52+
default:
53+
return 'bg-gradient-to-br from-gray-500 to-gray-600 dark:from-gray-400 dark:to-gray-500';
54+
}
55+
};
56+
57+
interface CardLinkWrapperProps {
58+
children?: React.ReactNode;
59+
link?: string;
60+
}
61+
62+
const CardLinkWrapper: React.FC<CardLinkWrapperProps> = ({ children, link }) => {
63+
return link ? (
64+
<Link to={link} className="block h-full w-full">
65+
{children}
66+
</Link>
67+
) : (
68+
<div className="block h-full w-full cursor-default">{children}</div>
69+
);
70+
};
71+
72+
// Get indicator component based on card type
73+
const getIndicator = (title: string) => {
74+
if (title === 'Total Clusters') {
75+
return (
76+
<div className="flex h-10 items-end space-x-1">
77+
{[0.4, 0.7, 1, 0.6, 0.8].map((height, i) => (
78+
<motion.div
79+
key={i}
80+
className="w-1.5 rounded-t bg-blue-500/70 dark:bg-blue-400/70"
81+
initial={{ height: 0 }}
82+
animate={{ height: `${height * 40}px` }}
83+
transition={{ delay: i * 0.1, duration: 0.5 }}
84+
></motion.div>
85+
))}
86+
</div>
87+
);
88+
}
89+
90+
if (title === 'Active Clusters') {
91+
return (
92+
<div className="flex h-10 w-10 items-center justify-center">
93+
<div className="flex -space-x-1.5">
94+
{[...Array(3)].map((_, i) => (
95+
<motion.div
96+
key={i}
97+
className="h-5 w-5 rounded-full border-2 border-white bg-emerald-500/80 dark:border-gray-800 dark:bg-emerald-400/80"
98+
initial={{ scale: 0, opacity: 0 }}
99+
animate={{ scale: 1, opacity: 1 }}
100+
transition={{ delay: i * 0.1, duration: 0.3 }}
101+
></motion.div>
102+
))}
103+
</div>
104+
</div>
105+
);
106+
}
107+
108+
if (title === 'Binding Policies') {
109+
return (
110+
<div className="relative flex h-10 w-10 items-center justify-center">
111+
<div className="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-900/30"></div>
112+
<FileText
113+
size={60}
114+
className="scale-[0.65] transform text-purple-600/80 dark:text-purple-400/80"
115+
/>
116+
</div>
117+
);
118+
}
119+
120+
if (title === 'Current Context') {
121+
return (
122+
<div className="relative flex h-10 w-10 items-center justify-center">
123+
<div className="absolute inset-0 rounded-full border-2 border-amber-500/30 bg-amber-500/10 dark:border-amber-400/30 dark:bg-amber-400/10"></div>
124+
<div className="absolute inset-0 rounded-full border-2 border-dashed border-amber-500/40 dark:border-amber-400/40"></div>
125+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-amber-500/20 to-amber-600/30 dark:from-amber-400/20 dark:to-amber-500/30">
126+
<Activity size={16} className="text-amber-600 dark:text-amber-400" />
127+
</div>
128+
</div>
129+
);
130+
}
131+
132+
return null;
133+
};
134+
const StatCard: React.FC<StatCardProps> = ({
135+
title,
136+
value,
137+
icon: Icon,
138+
change,
139+
iconColor,
140+
isContext = false,
141+
link,
142+
variants = defaultVariants,
143+
}) => {
144+
// Determine if change is positive, negative or neutral
145+
const isPositive = typeof change === 'number' && change > 0;
146+
const isNegative = typeof change === 'number' && change < 0;
147+
148+
return (
149+
<CardLinkWrapper link={link}>
150+
<motion.div
151+
className={`flex flex-col rounded-xl border border-gray-100 p-6 shadow-sm transition-all duration-300 dark:border-gray-700 ${getGradient(iconColor)} relative overflow-hidden`}
152+
whileHover={{
153+
y: -4,
154+
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.12)',
155+
transition: { duration: 0.3, ease: [0.23, 1, 0.32, 1] },
156+
}}
157+
initial={{ opacity: 0, y: 10 }}
158+
animate={{ opacity: 1, y: 0 }}
159+
transition={{ duration: 0.4 }}
160+
variants={variants}
161+
>
162+
{/* Decorative background elements for visual interest without animation loops */}
163+
<div className="absolute -right-4 -top-4 h-16 w-16 rounded-full bg-gradient-to-br from-white/5 to-white/10 dark:from-gray-700/10 dark:to-gray-700/20"></div>
164+
<div className="absolute -bottom-6 -left-6 h-24 w-24 rounded-full bg-gradient-to-tl from-white/5 to-white/0 dark:from-gray-700/5 dark:to-transparent"></div>
165+
166+
<div className="mb-4 flex items-center justify-between">
167+
<div className="flex items-center">
168+
<div
169+
className={`rounded-xl p-2.5 ${getIconGradient(iconColor)} mr-3 text-white shadow-lg`}
170+
>
171+
{React.createElement(Icon, { size: 18 })}
172+
</div>
173+
<span className="text-sm font-medium text-gray-700 transition-colors dark:text-gray-300">
174+
{title}
175+
</span>
176+
</div>
177+
</div>
178+
179+
<div className="mt-1 flex items-end justify-between">
180+
<div className="min-w-0 flex-grow">
181+
<div className="flex items-center">
182+
<h3 className="truncate text-3xl font-bold text-gray-900 transition-colors dark:text-gray-50">
183+
{value}
184+
</h3>
185+
{isContext && (
186+
<div className="ml-2 h-2.5 w-2.5 rounded-full bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.6)]"></div>
187+
)}
188+
</div>
189+
{change !== undefined && (
190+
<div className="mt-2.5 flex w-fit items-center rounded-full bg-gray-50 px-3 py-1 dark:bg-gray-800/50">
191+
{isPositive && <ChevronUp size={16} className="mr-1.5 text-emerald-500" />}
192+
{isNegative && <ChevronDown size={16} className="mr-1.5 text-red-500" />}
193+
<span
194+
className={
195+
isPositive
196+
? 'text-sm font-medium text-emerald-500'
197+
: isNegative
198+
? 'text-sm font-medium text-red-500'
199+
: 'text-sm font-medium text-gray-500 dark:text-gray-400'
200+
}
201+
>
202+
{Math.abs(change)}% {isPositive ? 'increase' : isNegative ? 'decrease' : 'change'}
203+
</span>
204+
</div>
205+
)}
206+
</div>
207+
208+
{/* Static visual indicators that don't use infinite animation loops */}
209+
{getIndicator(title)}
210+
</div>
211+
</motion.div>
212+
</CardLinkWrapper>
213+
);
214+
};
215+
216+
export default StatCard;

0 commit comments

Comments
 (0)