Files
smoothschedule/frontend/src/components/dashboard/MetricWidget.tsx
poduck dcb14503a2 feat: Dashboard redesign, plan permissions, and help docs improvements
Major updates including:
- Customizable dashboard with drag-and-drop widget grid layout
- Plan-based feature locking for plugins and tasks
- Comprehensive help documentation updates across all pages
- Plugin seeding in deployment process for all tenants
- Permission synchronization system for subscription plans
- QuotaOverageModal component and enhanced UX flows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 13:02:44 -05:00

91 lines
3.1 KiB
TypeScript

import React from 'react';
import { TrendingUp, TrendingDown, Minus, GripVertical, X } from 'lucide-react';
interface GrowthData {
weekly: { value: number; change: number };
monthly: { value: number; change: number };
}
interface MetricWidgetProps {
title: string;
value: number | string;
growth: GrowthData;
icon?: React.ReactNode;
isEditing?: boolean;
onRemove?: () => void;
}
const MetricWidget: React.FC<MetricWidgetProps> = ({
title,
value,
growth,
icon,
isEditing,
onRemove,
}) => {
const formatChange = (change: number) => {
if (change === 0) return '0%';
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
};
const getTrendIcon = (change: number) => {
if (change > 0) return <TrendingUp size={12} className="mr-1" />;
if (change < 0) return <TrendingDown size={12} className="mr-1" />;
return <Minus size={12} className="mr-1" />;
};
const getTrendClass = (change: number) => {
if (change > 0) return 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400';
if (change < 0) return 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400';
return 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300';
};
return (
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group">
{isEditing && (
<>
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
<GripVertical size={16} />
</div>
<button
onClick={onRemove}
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
>
<X size={14} />
</button>
</>
)}
<div className={isEditing ? 'pl-5' : ''}>
<div className="flex items-center gap-2 mb-2">
{icon && <span className="text-brand-500">{icon}</span>}
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</p>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
{value}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<div className="flex items-center gap-1">
<span className="text-gray-500 dark:text-gray-400">Week:</span>
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.weekly.change)}`}>
{getTrendIcon(growth.weekly.change)}
{formatChange(growth.weekly.change)}
</span>
</div>
<div className="flex items-center gap-1">
<span className="text-gray-500 dark:text-gray-400">Month:</span>
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.monthly.change)}`}>
{getTrendIcon(growth.monthly.change)}
{formatChange(growth.monthly.change)}
</span>
</div>
</div>
</div>
</div>
);
};
export default MetricWidget;