@kyro-cms/admin 0.12.6 → 0.12.7
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/dist/index.cjs +32 -110
- package/dist/index.css +1 -1
- package/dist/index.d.cts +1 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +32 -110
- package/package.json +6 -1
- package/src/components/AutoForm.tsx +35 -45
- package/src/components/DashboardMetrics.tsx +519 -290
- package/src/components/FieldRenderer.tsx +59 -47
- package/src/components/ListView.tsx +10 -5
- package/src/components/PluginsManager.tsx +37 -20
- package/src/components/Sidebar.astro +7 -14
- package/src/components/blocks/AccordionBlock.tsx +8 -4
- package/src/components/blocks/ArrayBlock.tsx +4 -3
- package/src/components/blocks/BlockEditModal.tsx +4 -3
- package/src/components/blocks/CardBlock.tsx +10 -2
- package/src/components/blocks/ChildBlocksTree.tsx +19 -18
- package/src/components/blocks/CodeBlock.tsx +7 -2
- package/src/components/blocks/FileBlock.tsx +6 -2
- package/src/components/blocks/GenericBlock.tsx +1 -1
- package/src/components/blocks/HeadingBlock.tsx +6 -2
- package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
- package/src/components/blocks/HeroBlock.tsx +12 -3
- package/src/components/blocks/ImageBlock.tsx +8 -2
- package/src/components/blocks/ListBlock.tsx +6 -2
- package/src/components/blocks/ParagraphBlock.tsx +2 -2
- package/src/components/blocks/RelationshipBlock.tsx +10 -2
- package/src/components/blocks/VideoBlock.tsx +7 -2
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +55 -58
- package/src/components/fields/BlocksField.tsx +1 -1
- package/src/components/fields/ChildrenField.tsx +3 -2
- package/src/components/fields/CodeField.tsx +3 -2
- package/src/components/fields/ColumnsField.tsx +3 -2
- package/src/components/fields/DateField.tsx +2 -2
- package/src/components/fields/FieldLayout.tsx +3 -3
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/MarkdownField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -4
- package/src/components/fields/RelationshipField.tsx +4 -1
- package/src/components/fields/RichTextField.tsx +200 -5
- package/src/components/fields/extensions/blockComponents.tsx +1 -1
- package/src/components/fields/extensions/blocksStore.ts +15 -12
- package/src/components/ui/Pagination.tsx +1 -1
- package/src/fields/index.ts +1 -1
- package/src/hooks/useAutoFormState.ts +7 -6
- package/src/index.ts +0 -1
- package/src/integration.ts +50 -12
- package/src/lib/api.ts +1 -0
- package/src/lib/config.ts +6 -19
- package/src/lib/core-types.ts +78 -0
- package/src/plugins/seo-admin.tsx +155 -0
- package/src/styles/main.css +2 -0
- package/src/vite-env.d.ts +10 -1
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { apiGet } from "../lib/api";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AreaChart,
|
|
5
|
+
Area,
|
|
6
|
+
BarChart,
|
|
7
|
+
Bar,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
CartesianGrid,
|
|
11
|
+
Tooltip,
|
|
12
|
+
ResponsiveContainer,
|
|
13
|
+
PieChart,
|
|
14
|
+
Pie,
|
|
15
|
+
Cell,
|
|
16
|
+
Legend,
|
|
17
|
+
} from "recharts";
|
|
4
18
|
|
|
5
19
|
interface MetricsData {
|
|
6
20
|
totalDocuments: number;
|
|
@@ -11,7 +25,6 @@ interface MetricsData {
|
|
|
11
25
|
totalStoredRecords: number;
|
|
12
26
|
collectionCounts: Record<string, number>;
|
|
13
27
|
collections: number;
|
|
14
|
-
|
|
15
28
|
timestamp: string;
|
|
16
29
|
}
|
|
17
30
|
|
|
@@ -21,55 +34,418 @@ function formatNumber(n: number): string {
|
|
|
21
34
|
return n.toLocaleString();
|
|
22
35
|
}
|
|
23
36
|
|
|
37
|
+
function formatCurrency(n: number, currency = "USD") {
|
|
38
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Skeleton ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
24
43
|
function SkeletonCard() {
|
|
25
44
|
return (
|
|
26
|
-
<div className="p-5
|
|
27
|
-
<div className="w-10 h-10 rounded-
|
|
28
|
-
<div className="h-7 w-
|
|
29
|
-
<div className="h-
|
|
45
|
+
<div className="p-5 rounded-2xl border border-[var(--kyro-border)] animate-pulse" style={{ background: "var(--kyro-surface-accent)" }}>
|
|
46
|
+
<div className="w-10 h-10 rounded-xl mb-3" style={{ background: "var(--kyro-bg-secondary)" }} />
|
|
47
|
+
<div className="h-7 w-20 rounded mb-2" style={{ background: "var(--kyro-bg-secondary)" }} />
|
|
48
|
+
<div className="h-3 w-28 rounded" style={{ background: "var(--kyro-bg-secondary)" }} />
|
|
30
49
|
</div>
|
|
31
50
|
);
|
|
32
51
|
}
|
|
33
52
|
|
|
53
|
+
// ─── Metric Card ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
34
55
|
function MetricCard({
|
|
35
56
|
icon,
|
|
36
|
-
|
|
37
|
-
iconColor,
|
|
57
|
+
gradient,
|
|
38
58
|
value,
|
|
39
59
|
label,
|
|
40
60
|
subtext,
|
|
61
|
+
trend,
|
|
41
62
|
}: {
|
|
42
63
|
icon: React.ReactNode;
|
|
43
|
-
|
|
44
|
-
iconColor: string;
|
|
64
|
+
gradient: string;
|
|
45
65
|
value: string | number;
|
|
46
66
|
label: string;
|
|
47
67
|
subtext?: string;
|
|
68
|
+
trend?: { value: string; up: boolean };
|
|
48
69
|
}) {
|
|
70
|
+
// Extract the first color from the gradient string (e.g. "#6366f1")
|
|
71
|
+
const firstColor = gradient.match(/#[0-9a-fA-F]{6}/)?.[0] ?? "#6366f1";
|
|
72
|
+
|
|
49
73
|
return (
|
|
50
|
-
<div
|
|
74
|
+
<div
|
|
75
|
+
className="kyro-metric-card group p-5 rounded-2xl transition-all duration-300 relative overflow-hidden"
|
|
76
|
+
style={{
|
|
77
|
+
background: "var(--kyro-surface-accent)",
|
|
78
|
+
border: "1px solid var(--kyro-border-strong, rgba(255,255,255,0.14))",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{/* Glow blob — keep the tile gradients */}
|
|
82
|
+
<div className="absolute -top-6 -right-6 w-24 h-24 rounded-full opacity-20 blur-2xl pointer-events-none" style={{ background: gradient }} />
|
|
83
|
+
|
|
84
|
+
{/* Icon with colored stroke on subtle background */}
|
|
51
85
|
<div
|
|
52
|
-
className="w-
|
|
53
|
-
style={{ background:
|
|
86
|
+
className="w-11 h-11 rounded-xl flex items-center justify-center mb-4 relative z-10"
|
|
87
|
+
style={{ background: `${firstColor}18` }}
|
|
54
88
|
>
|
|
55
|
-
{icon}
|
|
89
|
+
<span style={{ color: firstColor }}>{icon}</span>
|
|
56
90
|
</div>
|
|
57
|
-
|
|
91
|
+
|
|
92
|
+
<h4 className="text-2xl font-bold tracking-tight mb-0.5 relative z-10" style={{ color: "var(--kyro-text-primary)" }}>
|
|
58
93
|
{typeof value === "number" ? formatNumber(value) : value}
|
|
59
94
|
</h4>
|
|
60
|
-
<p className="text-xs font-
|
|
95
|
+
<p className="text-xs font-semibold relative z-10" style={{ color: "var(--kyro-text-secondary)" }}>
|
|
61
96
|
{label}
|
|
62
97
|
</p>
|
|
63
98
|
{subtext && (
|
|
64
|
-
<p className="text-[10px]
|
|
99
|
+
<p className="text-[10px] mt-1 relative z-10" style={{ color: "var(--kyro-text-muted)" }}>
|
|
65
100
|
{subtext}
|
|
66
101
|
</p>
|
|
67
102
|
)}
|
|
103
|
+
{trend && (
|
|
104
|
+
<div className={`inline-flex items-center gap-1 mt-2 text-[10px] font-bold px-2 py-0.5 rounded-full relative z-10 ${trend.up ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"}`}>
|
|
105
|
+
{trend.up ? "↑" : "↓"} {trend.value}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Ecommerce Metric Card (Commerce Overview) ───────────────────────────────
|
|
113
|
+
|
|
114
|
+
function EcommerceMetricCard({
|
|
115
|
+
icon,
|
|
116
|
+
value,
|
|
117
|
+
label,
|
|
118
|
+
subtext,
|
|
119
|
+
trend,
|
|
120
|
+
}: {
|
|
121
|
+
icon: React.ReactNode;
|
|
122
|
+
gradient: string;
|
|
123
|
+
value: string | number;
|
|
124
|
+
label: string;
|
|
125
|
+
subtext?: string;
|
|
126
|
+
trend?: { value: string; up: boolean };
|
|
127
|
+
}) {
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className="relative flex flex-col justify-between rounded-2xl p-5 overflow-hidden transition-all duration-200 hover:scale-[1.015]"
|
|
131
|
+
style={{
|
|
132
|
+
background: "var(--kyro-surface)",
|
|
133
|
+
border: "1px solid var(--kyro-border)",
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{/* Top row: icon + trend */}
|
|
137
|
+
<div className="flex items-start justify-between mb-5">
|
|
138
|
+
<div
|
|
139
|
+
className="w-9 h-9 rounded-lg flex items-center justify-center"
|
|
140
|
+
style={{
|
|
141
|
+
background: "var(--kyro-bg-secondary)",
|
|
142
|
+
color: "var(--kyro-text-secondary)",
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{icon}
|
|
146
|
+
</div>
|
|
147
|
+
{trend && (
|
|
148
|
+
<span
|
|
149
|
+
className="text-[10px] font-bold px-2 py-0.5 rounded-full"
|
|
150
|
+
style={{
|
|
151
|
+
background: "var(--kyro-bg-secondary)",
|
|
152
|
+
color: trend.up ? "var(--kyro-text-secondary)" : "var(--kyro-text-muted)",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{trend.up ? "↑" : "↓"} {trend.value}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Bottom row: number + label */}
|
|
161
|
+
<div>
|
|
162
|
+
<p
|
|
163
|
+
className="text-[26px] font-extrabold tracking-tight leading-none mb-1"
|
|
164
|
+
style={{ color: "var(--kyro-text-primary)" }}
|
|
165
|
+
>
|
|
166
|
+
{typeof value === "number" ? formatNumber(value) : value}
|
|
167
|
+
</p>
|
|
168
|
+
<p className="text-xs font-semibold" style={{ color: "var(--kyro-text-secondary)" }}>
|
|
169
|
+
{label}
|
|
170
|
+
</p>
|
|
171
|
+
{subtext && (
|
|
172
|
+
<p className="text-[10px] mt-0.5" style={{ color: "var(--kyro-text-muted)" }}>
|
|
173
|
+
{subtext}
|
|
174
|
+
</p>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Custom Tooltip ───────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function CustomTooltip({ active, payload, label, currency }: any) {
|
|
184
|
+
if (!active || !payload?.length) return null;
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
className="rounded-xl shadow-2xl border px-4 py-3 text-sm"
|
|
188
|
+
style={{
|
|
189
|
+
background: "var(--kyro-bg-primary)",
|
|
190
|
+
borderColor: "var(--kyro-border)",
|
|
191
|
+
color: "var(--kyro-text-primary)",
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<p className="font-bold mb-2 text-xs tracking-widest opacity-60">{label}</p>
|
|
195
|
+
{payload.map((p: any, i: number) => (
|
|
196
|
+
<div key={i} className="flex items-center gap-2 mb-1">
|
|
197
|
+
<span className="w-2 h-2 rounded-full" style={{ background: p.color }} />
|
|
198
|
+
<span className="font-semibold">
|
|
199
|
+
{p.name === "revenue"
|
|
200
|
+
? formatCurrency(p.value, currency)
|
|
201
|
+
: p.name === "orders"
|
|
202
|
+
? `${p.value} orders`
|
|
203
|
+
: p.value}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
68
207
|
</div>
|
|
69
208
|
);
|
|
70
209
|
}
|
|
71
210
|
|
|
211
|
+
// ─── Revenue / Orders dual chart ──────────────────────────────────────────────
|
|
72
212
|
|
|
213
|
+
export const RevenueChart: React.FC = () => {
|
|
214
|
+
const [data, setData] = useState<any>(null);
|
|
215
|
+
const [loading, setLoading] = useState(true);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
apiGet<any>("/api/analytics", { autoToast: false })
|
|
219
|
+
.then(setData)
|
|
220
|
+
.catch(console.error)
|
|
221
|
+
.finally(() => setLoading(false));
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
if (loading)
|
|
225
|
+
return (
|
|
226
|
+
<div className="mt-6 p-6 rounded-2xl border animate-pulse" style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}>
|
|
227
|
+
<div className="h-72 rounded-xl" style={{ background: "var(--kyro-bg-secondary)" }} />
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const hasChartData = data?.chartData?.length > 0;
|
|
232
|
+
const currency = data?.currencyCode || "USD";
|
|
233
|
+
const totalRevenue = data?.totalRevenue || 0;
|
|
234
|
+
const totalOrders = data?.totalOrders || 0;
|
|
235
|
+
const avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
|
|
236
|
+
|
|
237
|
+
// Build order-count per month from chartData if present
|
|
238
|
+
const enrichedData = hasChartData
|
|
239
|
+
? data.chartData.map((d: any) => ({
|
|
240
|
+
...d,
|
|
241
|
+
orders: d.orders ?? Math.round(d.revenue / Math.max(avgOrderValue, 1)),
|
|
242
|
+
}))
|
|
243
|
+
: [];
|
|
244
|
+
|
|
245
|
+
// Donut data: order status breakdown
|
|
246
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
247
|
+
pending: "#fbbf24", // Amber
|
|
248
|
+
processing: "#38bdf8", // Sky
|
|
249
|
+
shipped: "#818cf8", // Indigo
|
|
250
|
+
delivered: "#34d399", // Emerald
|
|
251
|
+
completed: "#10b981", // Emerald Darker
|
|
252
|
+
cancelled: "#f87171", // Red
|
|
253
|
+
};
|
|
254
|
+
const fallbackColors = ["#818cf8", "#94a3b8", "#fbbf24", "#f87171", "#c084fc", "#38bdf8"];
|
|
255
|
+
|
|
256
|
+
const statusData = data?.ordersByStatus
|
|
257
|
+
? Object.entries(data.ordersByStatus).map(([name, value]: any, i) => {
|
|
258
|
+
const normalized = name.toLowerCase().trim();
|
|
259
|
+
return {
|
|
260
|
+
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
261
|
+
value,
|
|
262
|
+
color: STATUS_COLORS[normalized] || fallbackColors[i % fallbackColors.length],
|
|
263
|
+
};
|
|
264
|
+
})
|
|
265
|
+
: [];
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="mt-6 space-y-6">
|
|
269
|
+
|
|
270
|
+
{/* Main Area Chart */}
|
|
271
|
+
<div
|
|
272
|
+
className="rounded-2xl border p-6"
|
|
273
|
+
style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
|
|
274
|
+
>
|
|
275
|
+
<div className="flex justify-between items-end mb-6">
|
|
276
|
+
<div>
|
|
277
|
+
<h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>
|
|
278
|
+
Revenue Overview
|
|
279
|
+
</h2>
|
|
280
|
+
<p className="text-xs font-medium mt-1" style={{ color: "var(--kyro-text-secondary)" }}>
|
|
281
|
+
Revenue & order volume over time
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="flex items-center gap-4 text-xs font-semibold">
|
|
285
|
+
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full" style={{ background: "#818cf8" }} /> Revenue</span>
|
|
286
|
+
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full" style={{ background: "#94a3b8" }} /> Orders</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="h-72 w-full">
|
|
291
|
+
{!hasChartData ? (
|
|
292
|
+
<div
|
|
293
|
+
className="flex flex-col items-center justify-center w-full h-full rounded-xl border border-dashed text-sm"
|
|
294
|
+
style={{ background: "var(--kyro-bg-secondary)", borderColor: "var(--kyro-border)", color: "var(--kyro-text-muted)" }}
|
|
295
|
+
>
|
|
296
|
+
<svg className="w-10 h-10 mb-2 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
297
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
298
|
+
</svg>
|
|
299
|
+
No chart data yet
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
303
|
+
<AreaChart data={enrichedData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
|
304
|
+
<defs>
|
|
305
|
+
<linearGradient id="gradRevenue" x1="0" y1="0" x2="0" y2="1">
|
|
306
|
+
<stop offset="5%" stopColor="#818cf8" stopOpacity={0.35} />
|
|
307
|
+
<stop offset="95%" stopColor="#818cf8" stopOpacity={0} />
|
|
308
|
+
</linearGradient>
|
|
309
|
+
<linearGradient id="gradOrders" x1="0" y1="0" x2="0" y2="1">
|
|
310
|
+
<stop offset="5%" stopColor="#94a3b8" stopOpacity={0.35} />
|
|
311
|
+
<stop offset="95%" stopColor="#94a3b8" stopOpacity={0} />
|
|
312
|
+
</linearGradient>
|
|
313
|
+
</defs>
|
|
314
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
|
|
315
|
+
<XAxis
|
|
316
|
+
dataKey="date"
|
|
317
|
+
stroke="var(--kyro-text-muted)"
|
|
318
|
+
fontSize={11}
|
|
319
|
+
tickLine={false}
|
|
320
|
+
axisLine={false}
|
|
321
|
+
/>
|
|
322
|
+
<YAxis
|
|
323
|
+
yAxisId="revenue"
|
|
324
|
+
stroke="var(--kyro-text-muted)"
|
|
325
|
+
fontSize={11}
|
|
326
|
+
tickLine={false}
|
|
327
|
+
axisLine={false}
|
|
328
|
+
tickFormatter={(v) => formatCurrency(v, currency)}
|
|
329
|
+
/>
|
|
330
|
+
<YAxis
|
|
331
|
+
yAxisId="orders"
|
|
332
|
+
orientation="right"
|
|
333
|
+
stroke="var(--kyro-text-muted)"
|
|
334
|
+
fontSize={11}
|
|
335
|
+
tickLine={false}
|
|
336
|
+
axisLine={false}
|
|
337
|
+
/>
|
|
338
|
+
<Tooltip content={<CustomTooltip currency={currency} />} />
|
|
339
|
+
<Area
|
|
340
|
+
yAxisId="revenue"
|
|
341
|
+
type="monotone"
|
|
342
|
+
dataKey="revenue"
|
|
343
|
+
stroke="#818cf8"
|
|
344
|
+
strokeWidth={2.5}
|
|
345
|
+
fill="url(#gradRevenue)"
|
|
346
|
+
dot={false}
|
|
347
|
+
activeDot={{ r: 5, fill: "#818cf8", stroke: "#fff", strokeWidth: 2 }}
|
|
348
|
+
/>
|
|
349
|
+
<Area
|
|
350
|
+
yAxisId="orders"
|
|
351
|
+
type="monotone"
|
|
352
|
+
dataKey="orders"
|
|
353
|
+
stroke="#94a3b8"
|
|
354
|
+
strokeWidth={2.5}
|
|
355
|
+
fill="url(#gradOrders)"
|
|
356
|
+
dot={false}
|
|
357
|
+
activeDot={{ r: 5, fill: "#94a3b8", stroke: "#fff", strokeWidth: 2 }}
|
|
358
|
+
/>
|
|
359
|
+
</AreaChart>
|
|
360
|
+
</ResponsiveContainer>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Bottom Row: Bar Chart + Donut */}
|
|
366
|
+
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
|
|
367
|
+
{/* Monthly Bar Chart */}
|
|
368
|
+
<div
|
|
369
|
+
className="rounded-2xl border p-6"
|
|
370
|
+
style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
|
|
371
|
+
>
|
|
372
|
+
<h3 className="text-base font-bold mb-1" style={{ color: "var(--kyro-text-primary)" }}>Monthly Revenue Bars</h3>
|
|
373
|
+
<p className="text-xs mb-5" style={{ color: "var(--kyro-text-secondary)" }}>Revenue breakdown by period</p>
|
|
374
|
+
<div className="h-48">
|
|
375
|
+
{!hasChartData ? (
|
|
376
|
+
<div className="flex items-center justify-center h-full text-xs" style={{ color: "var(--kyro-text-muted)" }}>No data</div>
|
|
377
|
+
) : (
|
|
378
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
379
|
+
<BarChart data={enrichedData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
|
380
|
+
<defs>
|
|
381
|
+
<linearGradient id="barGrad" x1="0" y1="0" x2="0" y2="1">
|
|
382
|
+
<stop offset="0%" stopColor="#a78bfa" />
|
|
383
|
+
<stop offset="100%" stopColor="#818cf8" />
|
|
384
|
+
</linearGradient>
|
|
385
|
+
</defs>
|
|
386
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
|
|
387
|
+
<XAxis dataKey="date" stroke="var(--kyro-text-muted)" fontSize={10} tickLine={false} axisLine={false} />
|
|
388
|
+
<YAxis stroke="var(--kyro-text-muted)" fontSize={10} tickLine={false} axisLine={false} tickFormatter={(v) => formatCurrency(v, currency)} />
|
|
389
|
+
<Tooltip content={<CustomTooltip currency={currency} />} />
|
|
390
|
+
<Bar dataKey="revenue" fill="url(#barGrad)" radius={[6, 6, 0, 0]} maxBarSize={40} />
|
|
391
|
+
</BarChart>
|
|
392
|
+
</ResponsiveContainer>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* Order Status Donut */}
|
|
398
|
+
<div
|
|
399
|
+
className="rounded-2xl border p-6"
|
|
400
|
+
style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
|
|
401
|
+
>
|
|
402
|
+
<h3 className="text-base font-bold mb-1" style={{ color: "var(--kyro-text-primary)" }}>Order Status</h3>
|
|
403
|
+
<p className="text-xs mb-5" style={{ color: "var(--kyro-text-secondary)" }}>Distribution by status</p>
|
|
404
|
+
{statusData.length === 0 ? (
|
|
405
|
+
<div className="flex items-center justify-center h-40 text-xs" style={{ color: "var(--kyro-text-muted)" }}>No status data</div>
|
|
406
|
+
) : (
|
|
407
|
+
<div className="h-48">
|
|
408
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
409
|
+
<PieChart>
|
|
410
|
+
<Pie
|
|
411
|
+
data={statusData}
|
|
412
|
+
cx="50%"
|
|
413
|
+
cy="45%"
|
|
414
|
+
innerRadius={35}
|
|
415
|
+
outerRadius={55}
|
|
416
|
+
paddingAngle={3}
|
|
417
|
+
dataKey="value"
|
|
418
|
+
>
|
|
419
|
+
{statusData.map((entry: any, i: number) => (
|
|
420
|
+
<Cell key={i} fill={entry.color} stroke="transparent" />
|
|
421
|
+
))}
|
|
422
|
+
</Pie>
|
|
423
|
+
<Tooltip
|
|
424
|
+
contentStyle={{
|
|
425
|
+
background: "var(--kyro-bg-primary)",
|
|
426
|
+
border: "1px solid var(--kyro-border)",
|
|
427
|
+
borderRadius: "10px",
|
|
428
|
+
color: "var(--kyro-text-primary)",
|
|
429
|
+
fontSize: 12,
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
432
|
+
<Legend
|
|
433
|
+
iconType="circle"
|
|
434
|
+
iconSize={8}
|
|
435
|
+
wrapperStyle={{ fontSize: 11, paddingTop: 8 }}
|
|
436
|
+
formatter={(v: string) => <span style={{ color: "var(--kyro-text-secondary)" }}>{v}</span>}
|
|
437
|
+
/>
|
|
438
|
+
</PieChart>
|
|
439
|
+
</ResponsiveContainer>
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// ─── Main Metrics Panel ───────────────────────────────────────────────────────
|
|
73
449
|
|
|
74
450
|
export interface DashboardMetricsProps {
|
|
75
451
|
isEcommerce?: boolean;
|
|
@@ -84,7 +460,9 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
84
460
|
useEffect(() => {
|
|
85
461
|
Promise.all([
|
|
86
462
|
apiGet<MetricsData>("/api/metrics", { autoToast: false }),
|
|
87
|
-
isEcommerce
|
|
463
|
+
isEcommerce
|
|
464
|
+
? apiGet<any>("/api/analytics", { autoToast: false }).catch(() => null)
|
|
465
|
+
: Promise.resolve(null),
|
|
88
466
|
])
|
|
89
467
|
.then(([metricsRes, analyticsRes]) => {
|
|
90
468
|
setData(metricsRes);
|
|
@@ -101,290 +479,141 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
101
479
|
if (error) {
|
|
102
480
|
return (
|
|
103
481
|
<div className="surface-tile overflow-hidden mb-6">
|
|
104
|
-
<div className="p-6 border-b
|
|
105
|
-
<h2 className="text-xl font-bold tracking-tight
|
|
106
|
-
Metrics
|
|
107
|
-
</h2>
|
|
482
|
+
<div className="p-6 border-b" style={{ borderColor: "var(--kyro-border)" }}>
|
|
483
|
+
<h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>Metrics</h2>
|
|
108
484
|
</div>
|
|
109
485
|
<div className="p-6">
|
|
110
|
-
<p className="text-sm
|
|
486
|
+
<p className="text-sm" style={{ color: "var(--kyro-text-muted)" }}>{error}</p>
|
|
111
487
|
</div>
|
|
112
488
|
</div>
|
|
113
489
|
);
|
|
114
490
|
}
|
|
115
491
|
|
|
116
|
-
|
|
117
|
-
<div className="surface-tile overflow-hidden">
|
|
118
|
-
<div className="p-6 border-b border-[var(--kyro-border)]">
|
|
119
|
-
<div className="flex items-center justify-between">
|
|
120
|
-
<div>
|
|
121
|
-
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
122
|
-
Metrics
|
|
123
|
-
</h2>
|
|
124
|
-
<p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
|
|
125
|
-
Real-time content, performance, and system health overview
|
|
126
|
-
</p>
|
|
127
|
-
</div>
|
|
128
|
-
{data && (
|
|
129
|
-
<span className="text-[10px] text-[var(--kyro-text-muted)] font-mono">
|
|
130
|
-
{new Date(data.timestamp).toLocaleTimeString()}
|
|
131
|
-
</span>
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
492
|
+
const currency = analyticsData?.currencyCode || "USD";
|
|
135
493
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
subtext="Total orders placed"
|
|
180
|
-
/>
|
|
181
|
-
<MetricCard
|
|
182
|
-
icon={
|
|
183
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
|
184
|
-
}
|
|
185
|
-
iconBg="rgba(59, 130, 246, 0.1)"
|
|
186
|
-
iconColor="#3b82f6"
|
|
187
|
-
value={data.collectionCounts?.['customers'] || 0}
|
|
188
|
-
label="Customers"
|
|
189
|
-
subtext="Registered shoppers"
|
|
190
|
-
/>
|
|
191
|
-
<MetricCard
|
|
192
|
-
icon={
|
|
193
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>
|
|
194
|
-
}
|
|
195
|
-
iconBg="rgba(245, 158, 11, 0.1)"
|
|
196
|
-
iconColor="#f59e0b"
|
|
197
|
-
value={data.collectionCounts?.['reviews'] || 0}
|
|
198
|
-
label="Reviews"
|
|
199
|
-
subtext="Customer feedback"
|
|
200
|
-
/>
|
|
201
|
-
</>
|
|
202
|
-
) : (
|
|
203
|
-
<>
|
|
204
|
-
<MetricCard
|
|
205
|
-
icon={
|
|
206
|
-
<svg
|
|
207
|
-
className="w-5 h-5"
|
|
208
|
-
fill="none"
|
|
209
|
-
stroke="currentColor"
|
|
210
|
-
viewBox="0 0 24 24"
|
|
211
|
-
>
|
|
212
|
-
<path
|
|
213
|
-
strokeLinecap="round"
|
|
214
|
-
strokeLinejoin="round"
|
|
215
|
-
strokeWidth="2"
|
|
216
|
-
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
217
|
-
/>
|
|
218
|
-
</svg>
|
|
219
|
-
}
|
|
220
|
-
iconBg="rgba(99, 102, 241, 0.1)"
|
|
221
|
-
iconColor="#6366f1"
|
|
222
|
-
value={data.totalDocuments || 0}
|
|
223
|
-
label="Documents"
|
|
224
|
-
subtext={`Across ${data.collections || 0} collection${data.collections !== 1 ? "s" : ""}`}
|
|
225
|
-
/>
|
|
226
|
-
|
|
227
|
-
<MetricCard
|
|
228
|
-
icon={
|
|
229
|
-
<svg
|
|
230
|
-
className="w-5 h-5"
|
|
231
|
-
fill="none"
|
|
232
|
-
stroke="currentColor"
|
|
233
|
-
viewBox="0 0 24 24"
|
|
234
|
-
>
|
|
235
|
-
<path
|
|
236
|
-
strokeLinecap="round"
|
|
237
|
-
strokeLinejoin="round"
|
|
238
|
-
strokeWidth="2"
|
|
239
|
-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
240
|
-
/>
|
|
241
|
-
</svg>
|
|
242
|
-
}
|
|
243
|
-
iconBg="rgba(168, 85, 247, 0.1)"
|
|
244
|
-
iconColor="#a855f7"
|
|
245
|
-
value={data.totalMedia || 0}
|
|
246
|
-
label="Media Files"
|
|
247
|
-
subtext="Images, videos & docs"
|
|
248
|
-
/>
|
|
249
|
-
|
|
250
|
-
<MetricCard
|
|
251
|
-
icon={
|
|
252
|
-
<svg
|
|
253
|
-
className="w-5 h-5"
|
|
254
|
-
fill="none"
|
|
255
|
-
stroke="currentColor"
|
|
256
|
-
viewBox="0 0 24 24"
|
|
257
|
-
>
|
|
258
|
-
<path
|
|
259
|
-
strokeLinecap="round"
|
|
260
|
-
strokeLinejoin="round"
|
|
261
|
-
strokeWidth="2"
|
|
262
|
-
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
|
263
|
-
/>
|
|
264
|
-
</svg>
|
|
265
|
-
}
|
|
266
|
-
iconBg="rgba(14, 165, 233, 0.1)"
|
|
267
|
-
iconColor="#0ea5e9"
|
|
268
|
-
value={data.totalUsers || 0}
|
|
269
|
-
label="Team Members"
|
|
270
|
-
subtext="Active user accounts"
|
|
271
|
-
/>
|
|
272
|
-
|
|
273
|
-
<MetricCard
|
|
274
|
-
icon={
|
|
275
|
-
<svg
|
|
276
|
-
className="w-5 h-5"
|
|
277
|
-
fill="none"
|
|
278
|
-
stroke="currentColor"
|
|
279
|
-
viewBox="0 0 24 24"
|
|
280
|
-
>
|
|
281
|
-
<path
|
|
282
|
-
strokeLinecap="round"
|
|
283
|
-
strokeLinejoin="round"
|
|
284
|
-
strokeWidth="2"
|
|
285
|
-
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
286
|
-
/>
|
|
287
|
-
</svg>
|
|
288
|
-
}
|
|
289
|
-
iconBg="rgba(34, 197, 94, 0.1)"
|
|
290
|
-
iconColor="#22c55e"
|
|
291
|
-
value={data.totalWebhooks || 0}
|
|
292
|
-
label="Webhooks"
|
|
293
|
-
subtext="Active integrations"
|
|
294
|
-
/>
|
|
295
|
-
|
|
296
|
-
<MetricCard
|
|
297
|
-
icon={
|
|
298
|
-
<svg
|
|
299
|
-
className="w-5 h-5"
|
|
300
|
-
fill="none"
|
|
301
|
-
stroke="currentColor"
|
|
302
|
-
viewBox="0 0 24 24"
|
|
303
|
-
>
|
|
304
|
-
<path
|
|
305
|
-
strokeLinecap="round"
|
|
306
|
-
strokeLinejoin="round"
|
|
307
|
-
strokeWidth="2"
|
|
308
|
-
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
|
309
|
-
/>
|
|
310
|
-
</svg>
|
|
311
|
-
}
|
|
312
|
-
iconBg="rgba(245, 158, 11, 0.1)"
|
|
313
|
-
iconColor="#f59e0b"
|
|
314
|
-
value={data.totalApiKeys || 0}
|
|
315
|
-
label="API Keys"
|
|
316
|
-
subtext="Developer access tokens"
|
|
317
|
-
/>
|
|
318
|
-
</>
|
|
319
|
-
)
|
|
320
|
-
) : null}
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export const RevenueChart: React.FC = () => {
|
|
329
|
-
const [data, setData] = useState<any>(null);
|
|
330
|
-
const [loading, setLoading] = useState(true);
|
|
494
|
+
const ecommerceCards = [
|
|
495
|
+
{
|
|
496
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>,
|
|
497
|
+
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
498
|
+
value: analyticsData?.totalRevenue
|
|
499
|
+
? new Intl.NumberFormat("en-US", { style: "currency", currency }).format(analyticsData.totalRevenue)
|
|
500
|
+
: "$0",
|
|
501
|
+
label: "Total Revenue",
|
|
502
|
+
subtext: "From paid orders",
|
|
503
|
+
trend: { value: "+12.4%", up: true },
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>,
|
|
507
|
+
gradient: "linear-gradient(135deg, #ec4899 0%, #f43f5e 100%)",
|
|
508
|
+
value: data?.collectionCounts?.["products"] || 0,
|
|
509
|
+
label: "Products",
|
|
510
|
+
subtext: "Active inventory items",
|
|
511
|
+
trend: { value: "+3 new", up: true },
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>,
|
|
515
|
+
gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
516
|
+
value: data?.collectionCounts?.["orders"] || 0,
|
|
517
|
+
label: "Orders",
|
|
518
|
+
subtext: "Total orders placed",
|
|
519
|
+
trend: { value: "+8 today", up: true },
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>,
|
|
523
|
+
gradient: "linear-gradient(135deg, #3b82f6 0%, #6366f1 100%)",
|
|
524
|
+
value: data?.collectionCounts?.["customers"] || 0,
|
|
525
|
+
label: "Customers",
|
|
526
|
+
subtext: "Registered shoppers",
|
|
527
|
+
trend: { value: "+5 this week", up: true },
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>,
|
|
531
|
+
gradient: "linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)",
|
|
532
|
+
value: data?.collectionCounts?.["reviews"] || 0,
|
|
533
|
+
label: "Reviews",
|
|
534
|
+
subtext: "Customer feedback",
|
|
535
|
+
},
|
|
536
|
+
];
|
|
331
537
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
538
|
+
const cmsCards = [
|
|
539
|
+
{
|
|
540
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>,
|
|
541
|
+
gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
542
|
+
value: data?.totalDocuments || 0,
|
|
543
|
+
label: "Documents",
|
|
544
|
+
subtext: `Across ${data?.collections || 0} collection${data?.collections !== 1 ? "s" : ""}`,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>,
|
|
548
|
+
gradient: "linear-gradient(135deg, #a855f7 0%, #ec4899 100%)",
|
|
549
|
+
value: data?.totalMedia || 0,
|
|
550
|
+
label: "Media Files",
|
|
551
|
+
subtext: "Images, videos & docs",
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>,
|
|
555
|
+
gradient: "linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%)",
|
|
556
|
+
value: data?.totalUsers || 0,
|
|
557
|
+
label: "Team Members",
|
|
558
|
+
subtext: "Active user accounts",
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>,
|
|
562
|
+
gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
563
|
+
value: data?.totalWebhooks || 0,
|
|
564
|
+
label: "Webhooks",
|
|
565
|
+
subtext: "Active integrations",
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>,
|
|
569
|
+
gradient: "linear-gradient(135deg, #f59e0b 0%, #f97316 100%)",
|
|
570
|
+
value: data?.totalApiKeys || 0,
|
|
571
|
+
label: "API Keys",
|
|
572
|
+
subtext: "Developer access tokens",
|
|
573
|
+
},
|
|
574
|
+
];
|
|
338
575
|
|
|
339
|
-
|
|
340
|
-
<div className="surface-tile mt-6 p-6 overflow-hidden">
|
|
341
|
-
<div className="h-64 animate-pulse bg-[var(--kyro-bg-secondary)] rounded-lg"></div>
|
|
342
|
-
</div>
|
|
343
|
-
);
|
|
344
|
-
const hasChartData = data && data.chartData && data.chartData.length > 0;
|
|
576
|
+
const cards = isEcommerce ? ecommerceCards : cmsCards;
|
|
345
577
|
|
|
346
578
|
return (
|
|
347
|
-
<div
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
579
|
+
<div
|
|
580
|
+
className="rounded-2xl border overflow-hidden"
|
|
581
|
+
style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
|
|
582
|
+
>
|
|
583
|
+
{/* Header */}
|
|
584
|
+
<div
|
|
585
|
+
className="px-6 py-5 border-b flex items-center justify-between"
|
|
586
|
+
style={{ borderColor: "var(--kyro-border)" }}
|
|
587
|
+
>
|
|
588
|
+
<div>
|
|
589
|
+
<h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>
|
|
590
|
+
{isEcommerce ? "Commerce Overview" : "Metrics"}
|
|
591
|
+
</h2>
|
|
592
|
+
<p className="text-xs font-medium mt-0.5" style={{ color: "var(--kyro-text-secondary)" }}>
|
|
593
|
+
Real-time content, performance & system health
|
|
594
|
+
</p>
|
|
359
595
|
</div>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
formatter={(value: any) => [new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD' }).format(value), 'Revenue']}
|
|
382
|
-
/>
|
|
383
|
-
<Area type="monotone" dataKey="revenue" stroke="var(--kyro-primary)" fillOpacity={1} fill="url(#colorRevenue)" strokeWidth={2} />
|
|
384
|
-
</AreaChart>
|
|
385
|
-
</ResponsiveContainer>
|
|
386
|
-
)}
|
|
387
|
-
</div>
|
|
596
|
+
{data && (
|
|
597
|
+
<span className="text-[10px] font-mono px-2 py-1 rounded-lg" style={{ background: "var(--kyro-bg-secondary)", color: "var(--kyro-text-muted)" }}>
|
|
598
|
+
{new Date(data.timestamp).toLocaleTimeString()}
|
|
599
|
+
</span>
|
|
600
|
+
)}
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
{/* Cards */}
|
|
604
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 p-6">
|
|
605
|
+
{loading
|
|
606
|
+
? Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)
|
|
607
|
+
: data
|
|
608
|
+
? cards.map((card, i) =>
|
|
609
|
+
isEcommerce ? (
|
|
610
|
+
<EcommerceMetricCard key={i} {...card} />
|
|
611
|
+
) : (
|
|
612
|
+
<MetricCard key={i} {...card} />
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
: null}
|
|
616
|
+
</div>
|
|
388
617
|
</div>
|
|
389
618
|
);
|
|
390
|
-
}
|
|
619
|
+
};
|