Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 89 additions & 28 deletions frontend/src/pages/PluginManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
id: number;
}

interface Stat {
label: string;
value: number;
color: string;
iconBg: string;
iconColor: string;
Icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
}

export const PluginManager: React.FC = () => {
const { t } = useTranslation();
const { theme } = useTheme();
Expand Down Expand Up @@ -80,7 +89,7 @@
console.log('loadedPlugins: ', loadedPlugins);
loadPluginData();
loadAvailablePlugins();
}, []);

Check warning on line 92 in frontend/src/pages/PluginManager.tsx

View workflow job for this annotation

GitHub Actions / Frontend Checks

React Hook useEffect has missing dependencies: 'loadAvailablePlugins', 'loadPluginData', and 'loadedPlugins'. Either include them or remove the dependency array

const handleEnablePlugin = async (pluginID: number) => {
try {
Expand Down Expand Up @@ -271,41 +280,93 @@
</div>

{/* Stats */}
<div className="flex gap-4">
{[
{
label: t('plugins.list.total'),
value: availablePlugins.length,
color: themeStyles.colors.text.primary,
},
{
label: t('plugins.list.active'),
value: availablePlugins.filter(p => p.enabled).length,
color: themeStyles.colors.status.success,
},
{
label: t('plugins.list.inactive'),
value: availablePlugins.filter(p => !p.enabled).length,
color: themeStyles.colors.text.secondary,
},
].map((stat, index) => (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{(
[
{
label: t('plugins.list.total'),
value: availablePlugins.length,
color: themeStyles.colors.text.primary,
iconBg: themeStyles.colors.text.secondary + '15',
iconColor: themeStyles.colors.text.primary,
Icon: HiOutlinePuzzlePiece,
},
{
label: t('plugins.list.active'),
value: availablePlugins.filter(p => p.enabled).length,
color: themeStyles.colors.status.success,
iconBg: themeStyles.colors.status.success + '20',
iconColor: themeStyles.colors.status.success,
Icon: HiOutlineCheckCircle,
},
{
label: t('plugins.list.inactive'),
value: availablePlugins.filter(p => !p.enabled).length,
color: themeStyles.colors.text.secondary,
iconBg: themeStyles.colors.text.secondary + '20',
iconColor: themeStyles.colors.text.secondary,
Icon: HiOutlinePause,
},
] as Stat[]
).map((stat, index) => (
<motion.div
key={stat.label}
className="flex flex-col gap-1 rounded-xl p-4"
className="group relative flex cursor-pointer flex-col gap-3 overflow-hidden rounded-2xl p-5"
style={{
background: themeStyles.effects.glassMorphism.background,
border: `1px solid ${themeStyles.card.borderColor}`,
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
}}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: index * 0.05 }}
whileHover={{
y: -4,
boxShadow:
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
}}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<span className="text-2xl font-bold" style={{ color: stat.color }}>
{stat.value}
</span>
<span className="text-sm" style={{ color: themeStyles.colors.text.secondary }}>
{stat.label}
</span>
{/* Gradient overlay on hover */}
<div
className="absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{
background: `linear-gradient(135deg, ${stat.iconColor}05 0%, ${stat.iconColor}10 100%)`,
}}
/>

<div
className="relative z-10 flex h-12 w-12 items-center justify-center rounded-xl transition-all duration-300 group-hover:scale-105"
style={{
background: stat.iconBg,
boxShadow: '0 0 0 0 transparent',
}}
>
<stat.Icon
className="h-6 w-6 transition-transform duration-300 group-hover:scale-110"
style={{ color: stat.iconColor }}
/>
</div>

<div className="relative z-10 flex flex-col gap-1">
<span
className="origin-left text-3xl font-bold tracking-tight transition-transform duration-300 group-hover:scale-105"
style={{ color: stat.color }}
>
{stat.value}
</span>
<span
className="text-sm font-medium"
style={{ color: themeStyles.colors.text.secondary }}
>
{stat.label}
</span>
</div>

{/* Bottom accent line */}
<div
className="absolute bottom-0 left-0 h-0.5 w-0 transition-all duration-300 group-hover:w-full"
style={{ background: stat.iconColor }}
/>
</motion.div>
))}
</div>
Expand Down
Loading