@umituz/web-dashboard 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -1
- package/src/domains/analytics/components/AnalyticsCard.tsx +80 -0
- package/src/domains/analytics/components/AnalyticsChart.tsx +184 -0
- package/src/domains/analytics/components/AnalyticsLayout.tsx +150 -0
- package/src/domains/analytics/components/MetricCard.tsx +108 -0
- package/src/domains/analytics/components/index.ts +10 -0
- package/src/domains/analytics/hooks/index.ts +7 -0
- package/src/domains/analytics/hooks/useAnalytics.ts +178 -0
- package/src/domains/analytics/index.ts +62 -0
- package/src/domains/analytics/types/analytics.ts +291 -0
- package/src/domains/analytics/types/index.ts +26 -0
- package/src/domains/analytics/utils/analytics.ts +333 -0
- package/src/domains/analytics/utils/index.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Dashboard Layout System - Customizable, themeable dashboard layouts and settings",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"./auth/hooks": "./src/domains/auth/hooks/index.ts",
|
|
29
29
|
"./auth/utils": "./src/domains/auth/utils/index.ts",
|
|
30
30
|
"./auth/types": "./src/domains/auth/types/index.ts",
|
|
31
|
+
"./analytics": "./src/domains/analytics/index.ts",
|
|
32
|
+
"./analytics/components": "./src/domains/analytics/components/index.ts",
|
|
33
|
+
"./analytics/hooks": "./src/domains/analytics/hooks/index.ts",
|
|
34
|
+
"./analytics/utils": "./src/domains/analytics/utils/index.ts",
|
|
35
|
+
"./analytics/types": "./src/domains/analytics/types/index.ts",
|
|
31
36
|
"./package.json": "./package.json"
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
@@ -50,6 +55,7 @@
|
|
|
50
55
|
"class-variance-authority": "^0.7.1",
|
|
51
56
|
"clsx": "^2.1.1",
|
|
52
57
|
"lucide-react": "^0.577.0",
|
|
58
|
+
"recharts": "^2.15.0",
|
|
53
59
|
"tailwind-merge": "^3.5.0"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
@@ -73,6 +79,12 @@
|
|
|
73
79
|
"authentication",
|
|
74
80
|
"login",
|
|
75
81
|
"register",
|
|
82
|
+
"analytics",
|
|
83
|
+
"charts",
|
|
84
|
+
"metrics",
|
|
85
|
+
"kpi",
|
|
86
|
+
"visualization",
|
|
87
|
+
"recharts",
|
|
76
88
|
"react",
|
|
77
89
|
"typescript",
|
|
78
90
|
"components",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Card Component
|
|
3
|
+
*
|
|
4
|
+
* Configurable analytics card with chart or metrics
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Loader2, AlertCircle } from "lucide-react";
|
|
8
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
+
import type { AnalyticsCardProps } from "../types/analytics";
|
|
10
|
+
import { MetricCard } from "./MetricCard";
|
|
11
|
+
import { AnalyticsChart } from "./AnalyticsChart";
|
|
12
|
+
|
|
13
|
+
export const AnalyticsCard = ({
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
chart,
|
|
17
|
+
metrics,
|
|
18
|
+
size = "md",
|
|
19
|
+
loading = false,
|
|
20
|
+
error,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
}: AnalyticsCardProps) => {
|
|
24
|
+
const sizeClasses = {
|
|
25
|
+
sm: "p-4",
|
|
26
|
+
md: "p-6",
|
|
27
|
+
lg: "p-8",
|
|
28
|
+
full: "p-6",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (loading) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn("bg-background border border-border rounded-xl", sizeClasses[size], className)}>
|
|
34
|
+
<div className="flex items-center justify-center h-48">
|
|
35
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
return (
|
|
43
|
+
<div className={cn("bg-background border border-border rounded-xl", sizeClasses[size], className)}>
|
|
44
|
+
<div className="flex items-center justify-center h-48 gap-3 text-destructive">
|
|
45
|
+
<AlertCircle className="h-5 w-5" />
|
|
46
|
+
<p className="text-sm">{error}</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn("bg-background border border-border rounded-xl", sizeClasses[size], className)}>
|
|
54
|
+
{/* Header */}
|
|
55
|
+
{(title || description) && (
|
|
56
|
+
<div className="mb-6">
|
|
57
|
+
{title && <h3 className="text-lg font-semibold text-foreground">{title}</h3>}
|
|
58
|
+
{description && <p className="text-sm text-muted-foreground mt-1">{description}</p>}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Chart */}
|
|
63
|
+
{chart && <AnalyticsChart config={chart} />}
|
|
64
|
+
|
|
65
|
+
{/* Metrics */}
|
|
66
|
+
{metrics && metrics.length > 0 && (
|
|
67
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
68
|
+
{metrics.map((metric) => (
|
|
69
|
+
<MetricCard key={metric.id} metric={metric} size="sm" />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Custom Content */}
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default AnalyticsCard;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Chart Component
|
|
3
|
+
*
|
|
4
|
+
* Configurable chart component using Recharts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
import {
|
|
9
|
+
LineChart,
|
|
10
|
+
BarChart,
|
|
11
|
+
AreaChart,
|
|
12
|
+
PieChart,
|
|
13
|
+
ResponsiveContainer,
|
|
14
|
+
XAxis,
|
|
15
|
+
YAxis,
|
|
16
|
+
CartesianGrid,
|
|
17
|
+
Tooltip,
|
|
18
|
+
Legend,
|
|
19
|
+
Line,
|
|
20
|
+
Bar,
|
|
21
|
+
Area,
|
|
22
|
+
Pie,
|
|
23
|
+
Cell,
|
|
24
|
+
} from "recharts";
|
|
25
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
26
|
+
import type { ChartConfig } from "../types/analytics";
|
|
27
|
+
import { generateChartColors } from "../utils/analytics";
|
|
28
|
+
|
|
29
|
+
interface AnalyticsChartProps {
|
|
30
|
+
/** Chart configuration */
|
|
31
|
+
config: ChartConfig;
|
|
32
|
+
/** Custom class name */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Custom height */
|
|
35
|
+
height?: number | string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const AnalyticsChart = ({ config, className, height }: AnalyticsChartProps) => {
|
|
39
|
+
const colors = useMemo(
|
|
40
|
+
() => config.colors || generateChartColors(config.yAxisKeys?.length || 5),
|
|
41
|
+
[config.colors, config.yAxisKeys]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const renderChart = () => {
|
|
45
|
+
const commonProps = {
|
|
46
|
+
data: config.data,
|
|
47
|
+
margin: { top: 10, right: 10, left: 10, bottom: 10 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
switch (config.type) {
|
|
51
|
+
case "line":
|
|
52
|
+
return (
|
|
53
|
+
<LineChart {...commonProps}>
|
|
54
|
+
{config.showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-border/50" />}
|
|
55
|
+
<XAxis
|
|
56
|
+
dataKey={config.xAxisKey}
|
|
57
|
+
className="text-xs text-muted-foreground"
|
|
58
|
+
axisLine={false}
|
|
59
|
+
tickLine={false}
|
|
60
|
+
/>
|
|
61
|
+
<YAxis
|
|
62
|
+
className="text-xs text-muted-foreground"
|
|
63
|
+
axisLine={false}
|
|
64
|
+
tickLine={false}
|
|
65
|
+
/>
|
|
66
|
+
{config.showTooltip && <Tooltip />}
|
|
67
|
+
{config.showLegend && <Legend />}
|
|
68
|
+
{config.yAxisKeys?.map((key, index) => (
|
|
69
|
+
<Line
|
|
70
|
+
key={key}
|
|
71
|
+
type="monotone"
|
|
72
|
+
dataKey={key}
|
|
73
|
+
stroke={colors[index]}
|
|
74
|
+
strokeWidth={2}
|
|
75
|
+
dot={{ r: 4 }}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</LineChart>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
case "bar":
|
|
82
|
+
return (
|
|
83
|
+
<BarChart {...commonProps}>
|
|
84
|
+
{config.showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-border/50" />}
|
|
85
|
+
<XAxis
|
|
86
|
+
dataKey={config.xAxisKey}
|
|
87
|
+
className="text-xs text-muted-foreground"
|
|
88
|
+
axisLine={false}
|
|
89
|
+
tickLine={false}
|
|
90
|
+
/>
|
|
91
|
+
<YAxis
|
|
92
|
+
className="text-xs text-muted-foreground"
|
|
93
|
+
axisLine={false}
|
|
94
|
+
tickLine={false}
|
|
95
|
+
/>
|
|
96
|
+
{config.showTooltip && <Tooltip />}
|
|
97
|
+
{config.showLegend && <Legend />}
|
|
98
|
+
{config.yAxisKeys?.map((key, index) => (
|
|
99
|
+
<Bar key={key} dataKey={key} fill={colors[index]} radius={[4, 4, 0, 0]} />
|
|
100
|
+
))}
|
|
101
|
+
</BarChart>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
case "area":
|
|
105
|
+
return (
|
|
106
|
+
<AreaChart {...commonProps}>
|
|
107
|
+
{config.showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-border/50" />}
|
|
108
|
+
<XAxis
|
|
109
|
+
dataKey={config.xAxisKey}
|
|
110
|
+
className="text-xs text-muted-foreground"
|
|
111
|
+
axisLine={false}
|
|
112
|
+
tickLine={false}
|
|
113
|
+
/>
|
|
114
|
+
<YAxis
|
|
115
|
+
className="text-xs text-muted-foreground"
|
|
116
|
+
axisLine={false}
|
|
117
|
+
tickLine={false}
|
|
118
|
+
/>
|
|
119
|
+
{config.showTooltip && <Tooltip />}
|
|
120
|
+
{config.showLegend && <Legend />}
|
|
121
|
+
{config.yAxisKeys?.map((key, index) => (
|
|
122
|
+
<Area
|
|
123
|
+
key={key}
|
|
124
|
+
type="monotone"
|
|
125
|
+
dataKey={key}
|
|
126
|
+
stroke={colors[index]}
|
|
127
|
+
fill={colors[index]}
|
|
128
|
+
fillOpacity={0.3}
|
|
129
|
+
/>
|
|
130
|
+
))}
|
|
131
|
+
</AreaChart>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
case "pie":
|
|
135
|
+
case "donut":
|
|
136
|
+
return (
|
|
137
|
+
<PieChart {...commonProps}>
|
|
138
|
+
<Pie
|
|
139
|
+
data={config.data}
|
|
140
|
+
cx="50%"
|
|
141
|
+
cy="50%"
|
|
142
|
+
labelLine={false}
|
|
143
|
+
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
|
144
|
+
outerRadius={80}
|
|
145
|
+
innerRadius={config.type === "donut" ? 40 : 0}
|
|
146
|
+
dataKey="value"
|
|
147
|
+
>
|
|
148
|
+
{config.data.map((entry: any, index: number) => (
|
|
149
|
+
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
|
150
|
+
))}
|
|
151
|
+
</Pie>
|
|
152
|
+
{config.showTooltip && <Tooltip />}
|
|
153
|
+
{config.showLegend && <Legend />}
|
|
154
|
+
</PieChart>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
default:
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
160
|
+
Unsupported chart type: {config.type}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const chartHeight = height || config.height || 300;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
className={cn(
|
|
171
|
+
"w-full",
|
|
172
|
+
config.aspectRatio || "aspect-video",
|
|
173
|
+
className
|
|
174
|
+
)}
|
|
175
|
+
style={{ height: typeof chartHeight === "number" ? `${chartHeight}px` : chartHeight }}
|
|
176
|
+
>
|
|
177
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
178
|
+
{renderChart()}
|
|
179
|
+
</ResponsiveContainer>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default AnalyticsChart;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Layout Component
|
|
3
|
+
*
|
|
4
|
+
* Configurable analytics page layout with KPIs and charts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { RefreshCw, Download, Calendar } from "lucide-react";
|
|
8
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
+
import { Button } from "@umituz/web-design-system/atoms";
|
|
10
|
+
import type { AnalyticsLayoutProps } from "../types/analytics";
|
|
11
|
+
import { AnalyticsCard } from "./AnalyticsCard";
|
|
12
|
+
import { MetricCard } from "./MetricCard";
|
|
13
|
+
import { getDateRangePresets } from "../utils/analytics";
|
|
14
|
+
|
|
15
|
+
export const AnalyticsLayout = ({
|
|
16
|
+
title,
|
|
17
|
+
description,
|
|
18
|
+
showDateRange = true,
|
|
19
|
+
showRefresh = true,
|
|
20
|
+
showExport = true,
|
|
21
|
+
kpis,
|
|
22
|
+
charts,
|
|
23
|
+
headerContent,
|
|
24
|
+
children,
|
|
25
|
+
}: AnalyticsLayoutProps) => {
|
|
26
|
+
const dateRangePresets = getDateRangePresets();
|
|
27
|
+
|
|
28
|
+
const handleExport = async () => {
|
|
29
|
+
// In production, implement export functionality
|
|
30
|
+
console.log("Exporting analytics data...");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleRefresh = async () => {
|
|
34
|
+
// In production, refresh data
|
|
35
|
+
console.log("Refreshing analytics data...");
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="w-full space-y-6">
|
|
40
|
+
{/* Header */}
|
|
41
|
+
<div className="flex items-center justify-between">
|
|
42
|
+
<div>
|
|
43
|
+
{title && <h1 className="text-3xl font-bold text-foreground">{title}</h1>}
|
|
44
|
+
{description && <p className="text-muted-foreground mt-1">{description}</p>}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Actions */}
|
|
48
|
+
<div className="flex items-center gap-3">
|
|
49
|
+
{showDateRange && (
|
|
50
|
+
<div className="flex items-center gap-2 bg-background border border-border rounded-lg px-3 py-2">
|
|
51
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
52
|
+
<select className="bg-transparent text-sm text-foreground outline-none">
|
|
53
|
+
{dateRangePresets.map((preset) => (
|
|
54
|
+
<option key={preset.value} value={preset.value}>
|
|
55
|
+
{preset.label}
|
|
56
|
+
</option>
|
|
57
|
+
))}
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{showRefresh && (
|
|
63
|
+
<Button variant="ghost" size="sm" onClick={handleRefresh}>
|
|
64
|
+
<RefreshCw className="h-4 w-4" />
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{showExport && (
|
|
69
|
+
<Button variant="ghost" size="sm" onClick={handleExport}>
|
|
70
|
+
<Download className="h-4 w-4" />
|
|
71
|
+
Export
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{headerContent}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* KPI Cards */}
|
|
80
|
+
{kpis && (
|
|
81
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
82
|
+
<MetricCard
|
|
83
|
+
metric={{
|
|
84
|
+
id: "downloads",
|
|
85
|
+
name: "Downloads",
|
|
86
|
+
value: kpis.downloads.current,
|
|
87
|
+
previousValue: kpis.downloads.previous,
|
|
88
|
+
unit: "K",
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
<MetricCard
|
|
92
|
+
metric={{
|
|
93
|
+
id: "engagement",
|
|
94
|
+
name: "Engagement",
|
|
95
|
+
value: kpis.engagement.current,
|
|
96
|
+
previousValue: kpis.engagement.previous,
|
|
97
|
+
unit: "%",
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
<MetricCard
|
|
101
|
+
metric={{
|
|
102
|
+
id: "users",
|
|
103
|
+
name: "Users",
|
|
104
|
+
value: kpis.users.current,
|
|
105
|
+
previousValue: kpis.users.previous,
|
|
106
|
+
unit: "K",
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
<MetricCard
|
|
110
|
+
metric={{
|
|
111
|
+
id: "revenue",
|
|
112
|
+
name: "Revenue",
|
|
113
|
+
value: kpis.revenue.current,
|
|
114
|
+
previousValue: kpis.revenue.previous,
|
|
115
|
+
unit: "$",
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
<MetricCard
|
|
119
|
+
metric={{
|
|
120
|
+
id: "retention",
|
|
121
|
+
name: "Retention",
|
|
122
|
+
value: kpis.retention.current,
|
|
123
|
+
previousValue: kpis.retention.previous,
|
|
124
|
+
unit: "%",
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Charts */}
|
|
131
|
+
{charts && charts.length > 0 && (
|
|
132
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
133
|
+
{charts.map((chartConfig, index) => (
|
|
134
|
+
<AnalyticsCard
|
|
135
|
+
key={index}
|
|
136
|
+
title={chartConfig.title}
|
|
137
|
+
description={chartConfig.description}
|
|
138
|
+
chart={chartConfig}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Custom Content */}
|
|
145
|
+
{children}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default AnalyticsLayout;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric Card Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single metric with trend indicator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ArrowUp, ArrowDown, Minus } from "lucide-react";
|
|
8
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
+
import type { MetricCardProps } from "../types/analytics";
|
|
10
|
+
import { formatMetricValue, calculateGrowth } from "../utils/analytics";
|
|
11
|
+
|
|
12
|
+
export const MetricCard = ({
|
|
13
|
+
metric,
|
|
14
|
+
size = "md",
|
|
15
|
+
showTrend = true,
|
|
16
|
+
showIcon = true,
|
|
17
|
+
className,
|
|
18
|
+
onClick,
|
|
19
|
+
}: MetricCardProps) => {
|
|
20
|
+
const growth =
|
|
21
|
+
metric.previousValue !== undefined
|
|
22
|
+
? calculateGrowth(metric.value, metric.previousValue)
|
|
23
|
+
: 0;
|
|
24
|
+
|
|
25
|
+
const sizeClasses = {
|
|
26
|
+
sm: "p-4",
|
|
27
|
+
md: "p-6",
|
|
28
|
+
lg: "p-8",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const titleSizeClasses = {
|
|
32
|
+
sm: "text-sm",
|
|
33
|
+
md: "text-base",
|
|
34
|
+
lg: "text-lg",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const valueSizeClasses = {
|
|
38
|
+
sm: "text-xl",
|
|
39
|
+
md: "text-3xl",
|
|
40
|
+
lg: "text-4xl",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-background border border-border rounded-xl",
|
|
48
|
+
sizeClasses[size],
|
|
49
|
+
onClick && "cursor-pointer hover:border-primary/50 transition-colors",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex items-start justify-between">
|
|
54
|
+
<div className="flex-1">
|
|
55
|
+
{/* Title */}
|
|
56
|
+
<p
|
|
57
|
+
className={cn(
|
|
58
|
+
"text-muted-foreground font-medium mb-1",
|
|
59
|
+
titleSizeClasses[size]
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{metric.name}
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
{/* Value */}
|
|
66
|
+
<div className={cn("font-bold text-foreground", valueSizeClasses[size])}>
|
|
67
|
+
{formatMetricValue(metric)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Trend */}
|
|
71
|
+
{showTrend && metric.previousValue !== undefined && (
|
|
72
|
+
<div className="flex items-center gap-1 mt-2">
|
|
73
|
+
{growth > 0 ? (
|
|
74
|
+
<ArrowUp className="h-4 w-4 text-green-600 dark:text-green-500" />
|
|
75
|
+
) : growth < 0 ? (
|
|
76
|
+
<ArrowDown className="h-4 w-4 text-destructive" />
|
|
77
|
+
) : (
|
|
78
|
+
<Minus className="h-4 w-4 text-muted-foreground" />
|
|
79
|
+
)}
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
"text-sm font-medium",
|
|
83
|
+
growth > 0 && "text-green-600 dark:text-green-500",
|
|
84
|
+
growth < 0 && "text-destructive",
|
|
85
|
+
growth === 0 && "text-muted-foreground"
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{Math.abs(growth).toFixed(1)}%
|
|
89
|
+
</span>
|
|
90
|
+
<span className="text-sm text-muted-foreground">
|
|
91
|
+
vs last period
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Icon */}
|
|
98
|
+
{showIcon && metric.icon && (
|
|
99
|
+
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
|
100
|
+
<metric.icon className="h-6 w-6 text-primary" />
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default MetricCard;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Components
|
|
3
|
+
*
|
|
4
|
+
* Export all analytics components
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { MetricCard } from "./MetricCard";
|
|
8
|
+
export { AnalyticsChart } from "./AnalyticsChart";
|
|
9
|
+
export { AnalyticsCard } from "./AnalyticsCard";
|
|
10
|
+
export { AnalyticsLayout } from "./AnalyticsLayout";
|