@kyro-cms/admin 0.12.4 → 0.12.5
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 +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +9 -3
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +41 -38
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +11 -10
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +3 -1
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -19
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- package/src/pages/index_ALT.astro +0 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { apiGet } from "../lib/api";
|
|
3
|
+
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
|
3
4
|
|
|
4
5
|
interface MetricsData {
|
|
5
6
|
totalDocuments: number;
|
|
@@ -70,15 +71,24 @@ function MetricCard({
|
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
export
|
|
74
|
+
export interface DashboardMetricsProps {
|
|
75
|
+
isEcommerce?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce }) => {
|
|
74
79
|
const [data, setData] = useState<MetricsData | null>(null);
|
|
80
|
+
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
|
75
81
|
const [loading, setLoading] = useState(true);
|
|
76
82
|
const [error, setError] = useState<string | null>(null);
|
|
77
83
|
|
|
78
84
|
useEffect(() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
Promise.all([
|
|
86
|
+
apiGet<MetricsData>("/api/metrics", { autoToast: false }),
|
|
87
|
+
isEcommerce ? apiGet<any>("/api/analytics", { autoToast: false }).catch(() => null) : Promise.resolve(null)
|
|
88
|
+
])
|
|
89
|
+
.then(([metricsRes, analyticsRes]) => {
|
|
90
|
+
setData(metricsRes);
|
|
91
|
+
if (analyticsRes) setAnalyticsData(analyticsRes);
|
|
82
92
|
setError(null);
|
|
83
93
|
})
|
|
84
94
|
.catch((err) => {
|
|
@@ -86,14 +96,14 @@ export function DashboardMetrics() {
|
|
|
86
96
|
setError("Unable to load metrics");
|
|
87
97
|
})
|
|
88
98
|
.finally(() => setLoading(false));
|
|
89
|
-
}, []);
|
|
99
|
+
}, [isEcommerce]);
|
|
90
100
|
|
|
91
101
|
if (error) {
|
|
92
102
|
return (
|
|
93
|
-
<div className="surface-tile overflow-hidden">
|
|
103
|
+
<div className="surface-tile overflow-hidden mb-6">
|
|
94
104
|
<div className="p-6 border-b border-[var(--kyro-border)]">
|
|
95
105
|
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
96
|
-
|
|
106
|
+
Metrics
|
|
97
107
|
</h2>
|
|
98
108
|
</div>
|
|
99
109
|
<div className="p-6">
|
|
@@ -108,21 +118,8 @@ export function DashboardMetrics() {
|
|
|
108
118
|
<div className="p-6 border-b border-[var(--kyro-border)]">
|
|
109
119
|
<div className="flex items-center justify-between">
|
|
110
120
|
<div>
|
|
111
|
-
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]
|
|
112
|
-
|
|
113
|
-
className="w-5 h-5 text-[var(--kyro-primary)]"
|
|
114
|
-
fill="none"
|
|
115
|
-
stroke="currentColor"
|
|
116
|
-
viewBox="0 0 24 24"
|
|
117
|
-
>
|
|
118
|
-
<path
|
|
119
|
-
strokeLinecap="round"
|
|
120
|
-
strokeLinejoin="round"
|
|
121
|
-
strokeWidth="2"
|
|
122
|
-
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"
|
|
123
|
-
/>
|
|
124
|
-
</svg>
|
|
125
|
-
CMS Metrics
|
|
121
|
+
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
122
|
+
Metrics
|
|
126
123
|
</h2>
|
|
127
124
|
<p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
|
|
128
125
|
Real-time content, performance, and system health overview
|
|
@@ -147,31 +144,86 @@ export function DashboardMetrics() {
|
|
|
147
144
|
<SkeletonCard />
|
|
148
145
|
</>
|
|
149
146
|
) : data ? (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
className="w-5 h-5"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
147
|
+
isEcommerce ? (
|
|
148
|
+
<>
|
|
149
|
+
<MetricCard
|
|
150
|
+
icon={
|
|
151
|
+
<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>
|
|
152
|
+
}
|
|
153
|
+
iconBg="rgba(34, 197, 94, 0.1)"
|
|
154
|
+
iconColor="#22c55e"
|
|
155
|
+
value={analyticsData?.totalRevenue
|
|
156
|
+
? new Intl.NumberFormat('en-US', { style: 'currency', currency: analyticsData.currencyCode || 'USD' }).format(analyticsData.totalRevenue)
|
|
157
|
+
: "$0.00"}
|
|
158
|
+
label="Sales"
|
|
159
|
+
subtext="Total revenue generated"
|
|
160
|
+
/>
|
|
161
|
+
<MetricCard
|
|
162
|
+
icon={
|
|
163
|
+
<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>
|
|
164
|
+
}
|
|
165
|
+
iconBg="rgba(236, 72, 153, 0.1)"
|
|
166
|
+
iconColor="#ec4899"
|
|
167
|
+
value={data.collectionCounts?.['products'] || 0}
|
|
168
|
+
label="Products"
|
|
169
|
+
subtext="Active inventory items"
|
|
170
|
+
/>
|
|
171
|
+
<MetricCard
|
|
172
|
+
icon={
|
|
173
|
+
<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>
|
|
174
|
+
}
|
|
175
|
+
iconBg="rgba(16, 185, 129, 0.1)"
|
|
176
|
+
iconColor="#10b981"
|
|
177
|
+
value={data.collectionCounts?.['orders'] || 0}
|
|
178
|
+
label="Orders"
|
|
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
|
+
/>
|
|
173
226
|
|
|
174
|
-
{data.totalMedia !== undefined && (
|
|
175
227
|
<MetricCard
|
|
176
228
|
icon={
|
|
177
229
|
<svg
|
|
@@ -190,13 +242,11 @@ export function DashboardMetrics() {
|
|
|
190
242
|
}
|
|
191
243
|
iconBg="rgba(168, 85, 247, 0.1)"
|
|
192
244
|
iconColor="#a855f7"
|
|
193
|
-
value={data.totalMedia}
|
|
245
|
+
value={data.totalMedia || 0}
|
|
194
246
|
label="Media Files"
|
|
195
247
|
subtext="Images, videos & docs"
|
|
196
248
|
/>
|
|
197
|
-
)}
|
|
198
249
|
|
|
199
|
-
{data.totalUsers !== undefined && (
|
|
200
250
|
<MetricCard
|
|
201
251
|
icon={
|
|
202
252
|
<svg
|
|
@@ -215,13 +265,11 @@ export function DashboardMetrics() {
|
|
|
215
265
|
}
|
|
216
266
|
iconBg="rgba(14, 165, 233, 0.1)"
|
|
217
267
|
iconColor="#0ea5e9"
|
|
218
|
-
value={data.totalUsers}
|
|
268
|
+
value={data.totalUsers || 0}
|
|
219
269
|
label="Team Members"
|
|
220
270
|
subtext="Active user accounts"
|
|
221
271
|
/>
|
|
222
|
-
)}
|
|
223
272
|
|
|
224
|
-
{data.totalWebhooks !== undefined && (
|
|
225
273
|
<MetricCard
|
|
226
274
|
icon={
|
|
227
275
|
<svg
|
|
@@ -240,13 +288,11 @@ export function DashboardMetrics() {
|
|
|
240
288
|
}
|
|
241
289
|
iconBg="rgba(34, 197, 94, 0.1)"
|
|
242
290
|
iconColor="#22c55e"
|
|
243
|
-
value={data.totalWebhooks}
|
|
291
|
+
value={data.totalWebhooks || 0}
|
|
244
292
|
label="Webhooks"
|
|
245
293
|
subtext="Active integrations"
|
|
246
294
|
/>
|
|
247
|
-
)}
|
|
248
295
|
|
|
249
|
-
{data.totalApiKeys !== undefined && (
|
|
250
296
|
<MetricCard
|
|
251
297
|
icon={
|
|
252
298
|
<svg
|
|
@@ -265,12 +311,12 @@ export function DashboardMetrics() {
|
|
|
265
311
|
}
|
|
266
312
|
iconBg="rgba(245, 158, 11, 0.1)"
|
|
267
313
|
iconColor="#f59e0b"
|
|
268
|
-
value={data.totalApiKeys}
|
|
314
|
+
value={data.totalApiKeys || 0}
|
|
269
315
|
label="API Keys"
|
|
270
316
|
subtext="Developer access tokens"
|
|
271
317
|
/>
|
|
272
|
-
|
|
273
|
-
|
|
318
|
+
</>
|
|
319
|
+
)
|
|
274
320
|
) : null}
|
|
275
321
|
</div>
|
|
276
322
|
|
|
@@ -278,3 +324,67 @@ export function DashboardMetrics() {
|
|
|
278
324
|
</div>
|
|
279
325
|
);
|
|
280
326
|
}
|
|
327
|
+
|
|
328
|
+
export const RevenueChart: React.FC = () => {
|
|
329
|
+
const [data, setData] = useState<any>(null);
|
|
330
|
+
const [loading, setLoading] = useState(true);
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
apiGet<any>("/api/analytics", { autoToast: false })
|
|
334
|
+
.then(setData)
|
|
335
|
+
.catch(console.error)
|
|
336
|
+
.finally(() => setLoading(false));
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
if (loading) return (
|
|
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;
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div className="surface-tile mt-6 p-6 overflow-hidden">
|
|
348
|
+
<div className="mb-6 flex justify-between items-end">
|
|
349
|
+
<div>
|
|
350
|
+
<h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">Revenue Overview</h2>
|
|
351
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">Daily revenue and order volume</p>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="text-right">
|
|
354
|
+
<h3 className="text-2xl font-bold text-[var(--kyro-primary)]">
|
|
355
|
+
{new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD' }).format(data?.totalRevenue || 0)}
|
|
356
|
+
</h3>
|
|
357
|
+
<p className="text-xs text-[var(--kyro-text-muted)]">{data?.totalOrders || 0} total orders</p>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="h-72 w-full">
|
|
361
|
+
{!hasChartData ? (
|
|
362
|
+
<div className="flex flex-col items-center justify-center w-full h-full text-[var(--kyro-text-secondary)] bg-[var(--kyro-bg-secondary)] rounded-xl border border-dashed border-[var(--kyro-border)]">
|
|
363
|
+
<svg className="w-10 h-10 mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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" /></svg>
|
|
364
|
+
<p className="text-sm font-medium">No chart data to display</p>
|
|
365
|
+
</div>
|
|
366
|
+
) : (
|
|
367
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
368
|
+
<AreaChart data={data.chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
|
369
|
+
<defs>
|
|
370
|
+
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
|
371
|
+
<stop offset="5%" stopColor="var(--kyro-primary)" stopOpacity={0.3}/>
|
|
372
|
+
<stop offset="95%" stopColor="var(--kyro-primary)" stopOpacity={0}/>
|
|
373
|
+
</linearGradient>
|
|
374
|
+
</defs>
|
|
375
|
+
<XAxis dataKey="date" stroke="var(--kyro-text-muted)" fontSize={12} tickLine={false} axisLine={false} />
|
|
376
|
+
<YAxis stroke="var(--kyro-text-muted)" fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD', maximumFractionDigits: 0 }).format(value)} />
|
|
377
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
|
|
378
|
+
<Tooltip
|
|
379
|
+
contentStyle={{ backgroundColor: 'var(--kyro-bg-primary)', border: '1px solid var(--kyro-border)', borderRadius: '8px', color: 'var(--kyro-text-primary)' }}
|
|
380
|
+
itemStyle={{ color: 'var(--kyro-text-primary)' }}
|
|
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>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
@@ -14,6 +14,8 @@ import { useAutoFormStore } from "../lib/autoform-store";
|
|
|
14
14
|
import { PageHeader } from "./ui/PageHeader";
|
|
15
15
|
import { Badge } from "./ui/Badge";
|
|
16
16
|
import { SplitButton } from "./ui/SplitButton";
|
|
17
|
+
import { useTranslation } from "react-i18next";
|
|
18
|
+
import "../lib/i18n";
|
|
17
19
|
import { adminPath } from "../lib/paths";
|
|
18
20
|
import { resolveFieldValue } from "../lib/resolve-field-value";
|
|
19
21
|
|
|
@@ -41,6 +43,7 @@ export function DetailView({
|
|
|
41
43
|
onError,
|
|
42
44
|
mode = "collection",
|
|
43
45
|
}: DetailViewProps) {
|
|
46
|
+
const { t } = useTranslation();
|
|
44
47
|
const { confirm, alert } = useUIStore();
|
|
45
48
|
const [data, setData] = useState<Record<string, unknown>>({});
|
|
46
49
|
const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
|
|
@@ -160,8 +163,8 @@ export function DetailView({
|
|
|
160
163
|
|
|
161
164
|
if (!isAutosave) {
|
|
162
165
|
const isDraft = status === "draft" || (savedData as any)?.status === "draft";
|
|
163
|
-
if (isDraft) toast.warning("Draft saved");
|
|
164
|
-
else toast.success("Updated");
|
|
166
|
+
if (isDraft) toast.warning(t("toast.draftSaved", { defaultValue: "Draft saved" }));
|
|
167
|
+
else toast.success(t("toast.updated", { defaultValue: "Updated" }));
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
setTimeout(() => {
|
|
@@ -175,8 +178,8 @@ export function DetailView({
|
|
|
175
178
|
} catch (e: any) {
|
|
176
179
|
setSaveStatus("error");
|
|
177
180
|
if (!isAutosave) {
|
|
178
|
-
onError("Failed to save changes");
|
|
179
|
-
toast.error("Failed to save changes");
|
|
181
|
+
onError(t("toast.saveError", { defaultValue: "Failed to save changes" }));
|
|
182
|
+
toast.error(t("toast.saveError", { defaultValue: "Failed to save changes" }));
|
|
180
183
|
}
|
|
181
184
|
} finally {
|
|
182
185
|
setSaving(false);
|
|
@@ -194,11 +197,11 @@ export function DetailView({
|
|
|
194
197
|
} as any);
|
|
195
198
|
setStatus("published");
|
|
196
199
|
setPublishedAt(new Date().toISOString());
|
|
197
|
-
toast.success("Published successfully");
|
|
200
|
+
toast.success(t("toast.published", { defaultValue: "Published successfully" }));
|
|
198
201
|
onSave();
|
|
199
202
|
} catch {
|
|
200
|
-
onError("Failed to publish");
|
|
201
|
-
toast.error("Failed to publish");
|
|
203
|
+
onError(t("toast.publishError", { defaultValue: "Failed to publish" }));
|
|
204
|
+
toast.error(t("toast.publishError", { defaultValue: "Failed to publish" }));
|
|
202
205
|
} finally {
|
|
203
206
|
setSaving(false);
|
|
204
207
|
}
|
|
@@ -212,11 +215,11 @@ export function DetailView({
|
|
|
212
215
|
headers: { "X-Draft": "false" },
|
|
213
216
|
} as any);
|
|
214
217
|
setStatus("draft");
|
|
215
|
-
toast.warning("Document unpublished");
|
|
218
|
+
toast.warning(t("toast.unpublished", { defaultValue: "Document unpublished" }));
|
|
216
219
|
onSave();
|
|
217
220
|
} catch {
|
|
218
|
-
onError("Failed to unpublish");
|
|
219
|
-
toast.error("Failed to unpublish");
|
|
221
|
+
onError(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
|
|
222
|
+
toast.error(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
|
|
220
223
|
} finally {
|
|
221
224
|
setSaving(false);
|
|
222
225
|
}
|
|
@@ -226,14 +229,14 @@ export function DetailView({
|
|
|
226
229
|
try {
|
|
227
230
|
setIsDuplicating(true);
|
|
228
231
|
const response = await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false }) as { data?: { id?: string } };
|
|
229
|
-
toast.success("Document duplicated");
|
|
232
|
+
toast.success(t("toast.duplicated", { defaultValue: "Document duplicated" }));
|
|
230
233
|
if (response?.data?.id) {
|
|
231
234
|
window.location.href = `${adminPath}/${slug}/${response.data.id}`;
|
|
232
235
|
} else {
|
|
233
236
|
window.location.href = `${adminPath}/${slug}`;
|
|
234
237
|
}
|
|
235
238
|
} catch (e) {
|
|
236
|
-
toast.error((e as Error).message || "Failed to duplicate document");
|
|
239
|
+
toast.error((e as Error).message || t("toast.duplicateError", { defaultValue: "Failed to duplicate document" }));
|
|
237
240
|
} finally {
|
|
238
241
|
setIsDuplicating(false);
|
|
239
242
|
}
|
|
@@ -249,9 +252,9 @@ export function DetailView({
|
|
|
249
252
|
delete (copyPayload as any).status;
|
|
250
253
|
|
|
251
254
|
await navigator.clipboard.writeText(JSON.stringify(copyPayload));
|
|
252
|
-
toast.success("Document data copied to clipboard");
|
|
255
|
+
toast.success(t("toast.dataCopied", { defaultValue: "Document data copied to clipboard" }));
|
|
253
256
|
} catch (e) {
|
|
254
|
-
toast.error("Failed to copy document data");
|
|
257
|
+
toast.error(t("toast.copyError", { defaultValue: "Failed to copy document data" }));
|
|
255
258
|
}
|
|
256
259
|
};
|
|
257
260
|
|
|
@@ -272,16 +275,16 @@ export function DetailView({
|
|
|
272
275
|
updatedAt: prev.updatedAt,
|
|
273
276
|
status: prev.status,
|
|
274
277
|
}));
|
|
275
|
-
toast.success("Document data pasted");
|
|
278
|
+
toast.success(t("toast.dataPasted", { defaultValue: "Document data pasted" }));
|
|
276
279
|
} catch (e) {
|
|
277
|
-
toast.error("Clipboard does not contain valid document JSON");
|
|
280
|
+
toast.error(t("toast.pasteError", { defaultValue: "Clipboard does not contain valid document JSON" }));
|
|
278
281
|
}
|
|
279
282
|
};
|
|
280
283
|
|
|
281
284
|
const handleDeleteTrigger = () => {
|
|
282
285
|
confirm({
|
|
283
|
-
title:
|
|
284
|
-
message: "This action cannot be undone.",
|
|
286
|
+
title: t("detailView.deleteTitle", { defaultValue: "Delete {{label}}?", label }),
|
|
287
|
+
message: t("detailView.deleteMessage", { defaultValue: "This action cannot be undone." }),
|
|
285
288
|
variant: "danger",
|
|
286
289
|
onConfirm: async () => {
|
|
287
290
|
try {
|
|
@@ -349,15 +352,15 @@ export function DetailView({
|
|
|
349
352
|
<PageHeader
|
|
350
353
|
back={{ onClick: onBack }}
|
|
351
354
|
breadcrumbs={[
|
|
352
|
-
{ label: mode === "global" ? "Globals" : "Collections" },
|
|
355
|
+
{ label: mode === "global" ? t("detailView.globals", { defaultValue: "Globals" }) : t("detailView.collections", { defaultValue: "Collections" }) },
|
|
353
356
|
{
|
|
354
357
|
label: label,
|
|
355
358
|
href: mode === "collection" ? `${adminPath}/${slug}` : undefined
|
|
356
359
|
},
|
|
357
|
-
{ label: mode === "global" ? "Edit" : documentId ? "Edit" : "New" }
|
|
360
|
+
{ label: mode === "global" ? t("actions.edit", { defaultValue: "Edit" }) : documentId ? t("actions.edit", { defaultValue: "Edit" }) : t("actions.new", { defaultValue: "New" }) }
|
|
358
361
|
]}
|
|
359
362
|
title={
|
|
360
|
-
(mode === "global" ? label : ((resolveFieldValue(collection?.fields as any, data, collection?.admin?.useAsTitle || "title") as string) || data.name as string || documentId ||
|
|
363
|
+
(mode === "global" ? label : ((resolveFieldValue(collection?.fields as any, data, collection?.admin?.useAsTitle || "title") as string) || data.name as string || documentId || t("detailView.newDocument", { defaultValue: "New {{label}}", label: collection?.singularLabel || label })))
|
|
361
364
|
}
|
|
362
365
|
metadata={[
|
|
363
366
|
<Badge
|
|
@@ -366,7 +369,7 @@ export function DetailView({
|
|
|
366
369
|
dot
|
|
367
370
|
className="text-[10px] font-bold "
|
|
368
371
|
>
|
|
369
|
-
{status}
|
|
372
|
+
{t(`status.${status}`, { defaultValue: status })}
|
|
370
373
|
</Badge>
|
|
371
374
|
]}
|
|
372
375
|
/>
|
|
@@ -384,7 +387,7 @@ export function DetailView({
|
|
|
384
387
|
onViewHistory={() => {
|
|
385
388
|
window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
|
|
386
389
|
}}
|
|
387
|
-
onPreview={togglePreview}
|
|
390
|
+
onPreview={((collection as any)?.admin?.disablePreview || (global as any)?.admin?.disablePreview) ? undefined : togglePreview}
|
|
388
391
|
onDelete={handleDeleteTrigger}
|
|
389
392
|
onBack={onBack}
|
|
390
393
|
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
@@ -405,7 +408,7 @@ export function DetailView({
|
|
|
405
408
|
<div className="surface-tile p-4 md:p-8">
|
|
406
409
|
<div className="flex items-center justify-between mb-8 px-1">
|
|
407
410
|
<h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
|
|
408
|
-
Core Configuration
|
|
411
|
+
{t("detailView.coreConfiguration", { defaultValue: "Core Configuration" })}
|
|
409
412
|
</h2>
|
|
410
413
|
<div className="h-px flex-1 bg-[var(--kyro-border)] ml-6 opacity-30" />
|
|
411
414
|
</div>
|
|
@@ -434,7 +437,7 @@ export function DetailView({
|
|
|
434
437
|
disabled={isDeleting || saving}
|
|
435
438
|
className="kyro-btn kyro-btn-sm text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)]/10 w-full justify-start mt-2"
|
|
436
439
|
>
|
|
437
|
-
{isDeleting ? "Deleting..." : "Delete Document"}
|
|
440
|
+
{isDeleting ? t("actions.deleting", { defaultValue: "Deleting..." }) : t("actions.deleteDocument", { defaultValue: "Delete Document" })}
|
|
438
441
|
</button>
|
|
439
442
|
)}
|
|
440
443
|
<SplitButton
|
|
@@ -458,7 +461,7 @@ export function DetailView({
|
|
|
458
461
|
<div className="w-3 h-3 rounded-full bg-green-400/80"></div>
|
|
459
462
|
</div>
|
|
460
463
|
<div className="text-[10px] font-mono opacity-50 truncate max-w-[60%] bg-[var(--kyro-bg)] px-3 py-1 rounded">
|
|
461
|
-
{previewUrl || "Generating preview URL..."}
|
|
464
|
+
{previewUrl || t("detailView.generatingPreview", { defaultValue: "Generating preview URL..." })}
|
|
462
465
|
</div>
|
|
463
466
|
<div className="w-16"></div>
|
|
464
467
|
</div>
|
|
@@ -471,7 +474,7 @@ export function DetailView({
|
|
|
471
474
|
<iframe
|
|
472
475
|
src={previewUrl}
|
|
473
476
|
className="w-full h-full border-0 bg-white"
|
|
474
|
-
title="Preview"
|
|
477
|
+
title={t("tooltips.preview", { defaultValue: "Preview" })}
|
|
475
478
|
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
476
479
|
/>
|
|
477
480
|
)}
|
|
@@ -483,12 +486,12 @@ export function DetailView({
|
|
|
483
486
|
<div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
484
487
|
<div className="surface-tile p-4 md:p-8">
|
|
485
488
|
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
|
|
486
|
-
Metadata
|
|
489
|
+
{t("detailView.metadata", { defaultValue: "Metadata" })}
|
|
487
490
|
</h3>
|
|
488
491
|
<div className="space-y-4 md:space-y-6">
|
|
489
492
|
<div className="flex flex-col gap-2">
|
|
490
493
|
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
491
|
-
Dynamic Status
|
|
494
|
+
{t("detailView.dynamicStatus", { defaultValue: "Dynamic Status" })}
|
|
492
495
|
</span>
|
|
493
496
|
<div>
|
|
494
497
|
<span
|
|
@@ -500,7 +503,7 @@ export function DetailView({
|
|
|
500
503
|
</div>
|
|
501
504
|
<div className="flex flex-col gap-2">
|
|
502
505
|
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
503
|
-
Date Created
|
|
506
|
+
{t("detailView.dateCreated", { defaultValue: "Date Created" })}
|
|
504
507
|
</span>
|
|
505
508
|
<span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
|
|
506
509
|
{createdAt
|
|
@@ -508,12 +511,12 @@ export function DetailView({
|
|
|
508
511
|
dateStyle: "medium",
|
|
509
512
|
timeStyle: "short",
|
|
510
513
|
})
|
|
511
|
-
: "N/A"}
|
|
514
|
+
: t("common.na", { defaultValue: "N/A" })}
|
|
512
515
|
</span>
|
|
513
516
|
</div>
|
|
514
517
|
<div className="flex flex-col gap-2">
|
|
515
518
|
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
516
|
-
Last Modified
|
|
519
|
+
{t("detailView.lastModified", { defaultValue: "Last Modified" })}
|
|
517
520
|
</span>
|
|
518
521
|
<span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
|
|
519
522
|
{updatedAt
|
|
@@ -521,13 +524,13 @@ export function DetailView({
|
|
|
521
524
|
dateStyle: "medium",
|
|
522
525
|
timeStyle: "short",
|
|
523
526
|
})
|
|
524
|
-
: "Just now"}
|
|
527
|
+
: t("detailView.justNow", { defaultValue: "Just now" })}
|
|
525
528
|
</span>
|
|
526
529
|
</div>
|
|
527
530
|
{publishedAt && (
|
|
528
531
|
<div className="flex flex-col gap-2">
|
|
529
532
|
<span className="text-[10px] font-bold tracking-widest opacity-40">
|
|
530
|
-
Public At
|
|
533
|
+
{t("detailView.publicAt", { defaultValue: "Public At" })}
|
|
531
534
|
</span>
|
|
532
535
|
<span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
|
|
533
536
|
{new Date(publishedAt).toLocaleString("en-US", {
|
|
@@ -542,7 +545,7 @@ export function DetailView({
|
|
|
542
545
|
|
|
543
546
|
<div className="surface-tile p-4 md:p-8 bg-[var(--kyro-bg-secondary)]">
|
|
544
547
|
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-3 md:mb-4">
|
|
545
|
-
Quick Links
|
|
548
|
+
{t("detailView.quickLinks", { defaultValue: "Quick Links" })}
|
|
546
549
|
</h3>
|
|
547
550
|
<div className="space-y-2 md:space-y-3">
|
|
548
551
|
<button
|
|
@@ -551,7 +554,7 @@ export function DetailView({
|
|
|
551
554
|
disabled={isDuplicating || saving}
|
|
552
555
|
className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
|
|
553
556
|
>
|
|
554
|
-
{isDuplicating ? "Duplicating..." : "Duplicate Document"}
|
|
557
|
+
{isDuplicating ? t("actions.duplicating", { defaultValue: "Duplicating..." }) : t("actions.duplicateDocument", { defaultValue: "Duplicate Document" })}
|
|
555
558
|
</button>
|
|
556
559
|
{previewUrl && (
|
|
557
560
|
<button
|
|
@@ -559,7 +562,7 @@ export function DetailView({
|
|
|
559
562
|
onClick={() => window.open(previewUrl, "_blank")}
|
|
560
563
|
className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
|
|
561
564
|
>
|
|
562
|
-
Open Preview in New Tab
|
|
565
|
+
{t("detailView.openPreview", { defaultValue: "Open Preview in New Tab" })}
|
|
563
566
|
</button>
|
|
564
567
|
)}
|
|
565
568
|
<button
|
|
@@ -567,7 +570,7 @@ export function DetailView({
|
|
|
567
570
|
onClick={handleDeleteTrigger}
|
|
568
571
|
className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start text-[var(--kyro-error)] hover:bg-[var(--kyro-danger-bg)]"
|
|
569
572
|
>
|
|
570
|
-
Delete Entry
|
|
573
|
+
{t("actions.deleteEntry", { defaultValue: "Delete Entry" })}
|
|
571
574
|
</button>
|
|
572
575
|
</div>
|
|
573
576
|
</div>
|
|
@@ -21,6 +21,7 @@ import { useUIStore, toast } from "../lib/stores";
|
|
|
21
21
|
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
22
22
|
import { PageHeader } from "./ui/PageHeader";
|
|
23
23
|
import { Badge } from "./ui/Badge";
|
|
24
|
+
import { useTranslation } from "react-i18next";
|
|
24
25
|
|
|
25
26
|
// @ts-ignore
|
|
26
27
|
const API_BASE = typeof __KYRO_API_PATH__ !== 'undefined' ? __KYRO_API_PATH__ : '/api';
|
|
@@ -34,6 +35,7 @@ interface ApiKey {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function DeveloperCenter({ collections }: { collections: Record<string, unknown> }) {
|
|
38
|
+
const { t } = useTranslation();
|
|
37
39
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
38
40
|
const [loading, setLoading] = useState(false);
|
|
39
41
|
const [showKey, setShowKey] = useState<string | null>(null);
|
|
@@ -114,7 +116,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
|
|
|
114
116
|
return (
|
|
115
117
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
116
118
|
<PageHeader
|
|
117
|
-
title="Developer Center"
|
|
119
|
+
title={t("tooltips.developerCenter", { defaultValue: "Developer Center" })}
|
|
118
120
|
description="Provision access keys and explore the headless API ecosystem."
|
|
119
121
|
icon={Code2}
|
|
120
122
|
actions={[
|
|
@@ -206,7 +208,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
|
|
|
206
208
|
type="button"
|
|
207
209
|
onClick={() => handleRevokeKey(key.id)}
|
|
208
210
|
className="p-3.5 bg-red-500/5 text-red-500 rounded-2xl hover:bg-red-500/10 transition-all border border-transparent hover:border-red-500/30 self-start md:self-center"
|
|
209
|
-
title="Revoke Key"
|
|
211
|
+
title={t("tooltips.revokeKey", { defaultValue: "Revoke Key" })}
|
|
210
212
|
>
|
|
211
213
|
<Trash2 className="w-5 h-5" />
|
|
212
214
|
</button>
|
|
@@ -367,7 +369,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
|
|
|
367
369
|
<Modal
|
|
368
370
|
open={showCreateModal}
|
|
369
371
|
onClose={() => setShowCreateModal(false)}
|
|
370
|
-
title="Generate Access Token"
|
|
372
|
+
title={t("tooltips.generateAccessToken", { defaultValue: "Generate Access Token" })}
|
|
371
373
|
size="lg"
|
|
372
374
|
>
|
|
373
375
|
<ModalContent>
|