@questpie/admin 0.0.1 → 1.0.1
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/README.md +439 -424
- package/dist/auth-layout-M8K8_q5R.mjs +181 -0
- package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
- package/dist/bulk-upload-dialog-D7w7W1Hl.mjs +273 -0
- package/dist/bulk-upload-dialog-D7w7W1Hl.mjs.map +1 -0
- package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
- package/dist/card-BKHjBQfw.mjs.map +1 -0
- package/dist/client/styles/index.css +434 -0
- package/dist/client-DbpZKSgH.d.mts +13585 -0
- package/dist/client-DbpZKSgH.d.mts.map +1 -0
- package/dist/client-njX1rZmi.mjs +22612 -0
- package/dist/client-njX1rZmi.mjs.map +1 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +13 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
- package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
- package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
- package/dist/dashboard-page-mCY0pgZv.mjs +3 -0
- package/dist/dropzone-Do3awXKd.mjs +634 -0
- package/dist/dropzone-Do3awXKd.mjs.map +1 -0
- package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
- package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
- package/dist/forgot-password-page-CEwsdLwn.mjs +3 -0
- package/dist/index-B9Xwk4hi.d.mts +2753 -0
- package/dist/index-B9Xwk4hi.d.mts.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +13 -0
- package/dist/login-page-BUnpCbCa.mjs +3 -0
- package/dist/login-page-CP4gA-dl.mjs +298 -0
- package/dist/login-page-CP4gA-dl.mjs.map +1 -0
- package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
- package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
- package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
- package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
- package/dist/reset-password-page-CufHz3h3.mjs +3 -0
- package/dist/runtime-6VZM878K.mjs +69 -0
- package/dist/runtime-6VZM878K.mjs.map +1 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
- package/dist/server.d.mts +250 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +832 -0
- package/dist/server.mjs.map +1 -0
- package/dist/setup-page-BNNzt_Z6.mjs +3 -0
- package/dist/setup-page-YAP_fzqh.mjs +264 -0
- package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
- package/dist/shared.d.mts +57 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +3 -0
- package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
- package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
- package/package.json +48 -198
- package/.turbo/turbo-build.log +0 -108
- package/CHANGELOG.md +0 -10
- package/STATUS.md +0 -917
- package/VALIDATION.md +0 -602
- package/components.json +0 -24
- package/dist/__tests__/setup.mjs +0 -38
- package/dist/__tests__/test-utils.mjs +0 -45
- package/dist/__tests__/vitest.d.mjs +0 -3
- package/dist/components/admin-app.mjs +0 -69
- package/dist/components/fields/array-field.mjs +0 -190
- package/dist/components/fields/checkbox-field.mjs +0 -34
- package/dist/components/fields/custom-field.mjs +0 -32
- package/dist/components/fields/date-field.mjs +0 -41
- package/dist/components/fields/datetime-field.mjs +0 -42
- package/dist/components/fields/email-field.mjs +0 -37
- package/dist/components/fields/embedded-collection.mjs +0 -253
- package/dist/components/fields/field-types.mjs +0 -1
- package/dist/components/fields/field-utils.mjs +0 -10
- package/dist/components/fields/field-wrapper.mjs +0 -34
- package/dist/components/fields/index.mjs +0 -23
- package/dist/components/fields/json-field.mjs +0 -243
- package/dist/components/fields/locale-badge.mjs +0 -16
- package/dist/components/fields/number-field.mjs +0 -39
- package/dist/components/fields/password-field.mjs +0 -37
- package/dist/components/fields/relation-field.mjs +0 -104
- package/dist/components/fields/relation-picker.mjs +0 -229
- package/dist/components/fields/relation-select.mjs +0 -188
- package/dist/components/fields/rich-text-editor/index.mjs +0 -897
- package/dist/components/fields/select-field.mjs +0 -41
- package/dist/components/fields/switch-field.mjs +0 -34
- package/dist/components/fields/text-field.mjs +0 -38
- package/dist/components/fields/textarea-field.mjs +0 -38
- package/dist/components/index.mjs +0 -59
- package/dist/components/primitives/checkbox-input.mjs +0 -127
- package/dist/components/primitives/date-input.mjs +0 -303
- package/dist/components/primitives/index.mjs +0 -12
- package/dist/components/primitives/number-input.mjs +0 -104
- package/dist/components/primitives/select-input.mjs +0 -177
- package/dist/components/primitives/tag-input.mjs +0 -135
- package/dist/components/primitives/text-input.mjs +0 -39
- package/dist/components/primitives/textarea-input.mjs +0 -37
- package/dist/components/primitives/toggle-input.mjs +0 -31
- package/dist/components/primitives/types.mjs +0 -12
- package/dist/components/ui/accordion.mjs +0 -55
- package/dist/components/ui/avatar.mjs +0 -54
- package/dist/components/ui/badge.mjs +0 -34
- package/dist/components/ui/button.mjs +0 -48
- package/dist/components/ui/checkbox.mjs +0 -21
- package/dist/components/ui/combobox.mjs +0 -163
- package/dist/components/ui/dialog.mjs +0 -95
- package/dist/components/ui/dropdown-menu.mjs +0 -138
- package/dist/components/ui/field.mjs +0 -113
- package/dist/components/ui/input-group.mjs +0 -82
- package/dist/components/ui/input.mjs +0 -17
- package/dist/components/ui/label.mjs +0 -15
- package/dist/components/ui/popover.mjs +0 -56
- package/dist/components/ui/scroll-area.mjs +0 -38
- package/dist/components/ui/select.mjs +0 -100
- package/dist/components/ui/separator.mjs +0 -16
- package/dist/components/ui/sheet.mjs +0 -90
- package/dist/components/ui/sidebar.mjs +0 -387
- package/dist/components/ui/skeleton.mjs +0 -14
- package/dist/components/ui/spinner.mjs +0 -16
- package/dist/components/ui/switch.mjs +0 -22
- package/dist/components/ui/table.mjs +0 -68
- package/dist/components/ui/tabs.mjs +0 -48
- package/dist/components/ui/textarea.mjs +0 -15
- package/dist/components/ui/tooltip.mjs +0 -44
- package/dist/config/component-registry.mjs +0 -38
- package/dist/config/index.mjs +0 -129
- package/dist/hooks/admin-provider.mjs +0 -70
- package/dist/hooks/index.mjs +0 -7
- package/dist/hooks/store.mjs +0 -178
- package/dist/hooks/use-collection-db.mjs +0 -146
- package/dist/hooks/use-collection.mjs +0 -112
- package/dist/hooks/use-global.mjs +0 -46
- package/dist/hooks/use-mobile.mjs +0 -20
- package/dist/lib/utils.mjs +0 -10
- package/dist/styles/index.css +0 -336
- package/dist/styles/index.mjs +0 -1
- package/dist/utils/index.mjs +0 -9
- package/dist/views/auth/auth-layout.mjs +0 -52
- package/dist/views/auth/index.mjs +0 -6
- package/dist/views/auth/login-form.mjs +0 -156
- package/dist/views/collection/auto-form-fields.mjs +0 -525
- package/dist/views/collection/collection-form.mjs +0 -91
- package/dist/views/collection/collection-list.mjs +0 -76
- package/dist/views/collection/form-field.mjs +0 -42
- package/dist/views/collection/index.mjs +0 -6
- package/dist/views/common/index.mjs +0 -4
- package/dist/views/common/locale-switcher.mjs +0 -39
- package/dist/views/common/version-history.mjs +0 -272
- package/dist/views/index.mjs +0 -9
- package/dist/views/layout/admin-layout.mjs +0 -40
- package/dist/views/layout/admin-router.mjs +0 -95
- package/dist/views/layout/admin-sidebar.mjs +0 -63
- package/dist/views/layout/index.mjs +0 -5
- package/src/__tests__/setup.ts +0 -44
- package/src/__tests__/test-utils.tsx +0 -49
- package/src/__tests__/vitest.d.ts +0 -9
- package/src/components/admin-app.tsx +0 -221
- package/src/components/fields/array-field.tsx +0 -237
- package/src/components/fields/checkbox-field.tsx +0 -47
- package/src/components/fields/custom-field.tsx +0 -50
- package/src/components/fields/date-field.tsx +0 -65
- package/src/components/fields/datetime-field.tsx +0 -67
- package/src/components/fields/email-field.tsx +0 -51
- package/src/components/fields/embedded-collection.tsx +0 -315
- package/src/components/fields/field-types.ts +0 -162
- package/src/components/fields/field-utils.ts +0 -6
- package/src/components/fields/field-wrapper.tsx +0 -52
- package/src/components/fields/index.ts +0 -66
- package/src/components/fields/json-field.tsx +0 -440
- package/src/components/fields/locale-badge.tsx +0 -15
- package/src/components/fields/number-field.tsx +0 -57
- package/src/components/fields/password-field.tsx +0 -51
- package/src/components/fields/relation-field.tsx +0 -243
- package/src/components/fields/relation-picker.tsx +0 -402
- package/src/components/fields/relation-select.tsx +0 -327
- package/src/components/fields/rich-text-editor/index.tsx +0 -1337
- package/src/components/fields/select-field.tsx +0 -61
- package/src/components/fields/switch-field.tsx +0 -47
- package/src/components/fields/text-field.tsx +0 -55
- package/src/components/fields/textarea-field.tsx +0 -55
- package/src/components/index.ts +0 -40
- package/src/components/primitives/checkbox-input.tsx +0 -193
- package/src/components/primitives/date-input.tsx +0 -401
- package/src/components/primitives/index.ts +0 -24
- package/src/components/primitives/number-input.tsx +0 -132
- package/src/components/primitives/select-input.tsx +0 -296
- package/src/components/primitives/tag-input.tsx +0 -200
- package/src/components/primitives/text-input.tsx +0 -49
- package/src/components/primitives/textarea-input.tsx +0 -46
- package/src/components/primitives/toggle-input.tsx +0 -36
- package/src/components/primitives/types.ts +0 -235
- package/src/components/ui/accordion.tsx +0 -72
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -48
- package/src/components/ui/button.tsx +0 -53
- package/src/components/ui/card.tsx +0 -94
- package/src/components/ui/checkbox.tsx +0 -27
- package/src/components/ui/combobox.tsx +0 -290
- package/src/components/ui/dialog.tsx +0 -151
- package/src/components/ui/dropdown-menu.tsx +0 -254
- package/src/components/ui/field.tsx +0 -227
- package/src/components/ui/input-group.tsx +0 -149
- package/src/components/ui/input.tsx +0 -20
- package/src/components/ui/label.tsx +0 -18
- package/src/components/ui/popover.tsx +0 -88
- package/src/components/ui/scroll-area.tsx +0 -53
- package/src/components/ui/select.tsx +0 -192
- package/src/components/ui/separator.tsx +0 -23
- package/src/components/ui/sheet.tsx +0 -127
- package/src/components/ui/sidebar.tsx +0 -723
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/spinner.tsx +0 -10
- package/src/components/ui/switch.tsx +0 -32
- package/src/components/ui/table.tsx +0 -99
- package/src/components/ui/tabs.tsx +0 -82
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/tooltip.tsx +0 -70
- package/src/config/component-registry.ts +0 -190
- package/src/config/index.ts +0 -1099
- package/src/hooks/README.md +0 -269
- package/src/hooks/admin-provider.tsx +0 -110
- package/src/hooks/index.ts +0 -41
- package/src/hooks/store.ts +0 -248
- package/src/hooks/use-auth.ts +0 -168
- package/src/hooks/use-collection-db.ts +0 -209
- package/src/hooks/use-collection.ts +0 -156
- package/src/hooks/use-global.ts +0 -69
- package/src/hooks/use-mobile.ts +0 -21
- package/src/lib/utils.ts +0 -6
- package/src/styles/index.css +0 -340
- package/src/utils/index.ts +0 -6
- package/src/views/auth/auth-layout.tsx +0 -77
- package/src/views/auth/forgot-password-form.tsx +0 -192
- package/src/views/auth/index.ts +0 -21
- package/src/views/auth/login-form.tsx +0 -229
- package/src/views/auth/reset-password-form.tsx +0 -232
- package/src/views/collection/auto-form-fields.tsx +0 -982
- package/src/views/collection/collection-form.tsx +0 -186
- package/src/views/collection/collection-list.tsx +0 -223
- package/src/views/collection/form-field.tsx +0 -52
- package/src/views/collection/index.ts +0 -15
- package/src/views/common/index.ts +0 -8
- package/src/views/common/locale-switcher.tsx +0 -45
- package/src/views/common/version-history.tsx +0 -406
- package/src/views/index.ts +0 -25
- package/src/views/layout/admin-layout.tsx +0 -117
- package/src/views/layout/admin-router.tsx +0 -206
- package/src/views/layout/admin-sidebar.tsx +0 -185
- package/src/views/layout/index.ts +0 -12
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -29
|
@@ -0,0 +1,2500 @@
|
|
|
1
|
+
import { _ as formatCollectionName, a as selectBasePath, f as useAdminStore, g as cn, h as Button, l as selectNavigate, r as selectAdmin, s as selectClient, v as useResolveText } from "./content-locales-provider-BXvuIgfg.mjs";
|
|
2
|
+
import { r as useScopedLocale } from "./runtime-6VZM878K.mjs";
|
|
3
|
+
import { i as CardDescription, n as CardAction, o as CardHeader, r as CardContent, s as CardTitle, t as Card } from "./card-BKHjBQfw.mjs";
|
|
4
|
+
import { ArrowClockwise, ArrowRight, ArrowsOutSimple, CaretDownIcon, Circle } from "@phosphor-icons/react";
|
|
5
|
+
import * as React$1 from "react";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { cva } from "class-variance-authority";
|
|
9
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
10
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
11
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
|
|
13
|
+
import { Accordion } from "@base-ui/react/accordion";
|
|
14
|
+
import { Tabs } from "@base-ui/react/tabs";
|
|
15
|
+
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
16
|
+
|
|
17
|
+
//#region src/client/components/ui/badge.tsx
|
|
18
|
+
const badgeVariants = cva("h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge", {
|
|
19
|
+
variants: { variant: {
|
|
20
|
+
default: "bg-primary/10 text-primary border-primary border backdrop-blur-sm [a]:hover:bg-primary/80",
|
|
21
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
22
|
+
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
23
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 ",
|
|
24
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
25
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
26
|
+
} },
|
|
27
|
+
defaultVariants: { variant: "default" }
|
|
28
|
+
});
|
|
29
|
+
function Badge({ className, variant = "default", render, ...props }) {
|
|
30
|
+
return useRender({
|
|
31
|
+
defaultTagName: "span",
|
|
32
|
+
props: mergeProps({ className: cn(badgeVariants({
|
|
33
|
+
className,
|
|
34
|
+
variant
|
|
35
|
+
})) }, props),
|
|
36
|
+
render,
|
|
37
|
+
state: {
|
|
38
|
+
slot: "badge",
|
|
39
|
+
variant
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/client/views/collection/cells/primitive-cells.tsx
|
|
46
|
+
/**
|
|
47
|
+
* Ultra-simple default cell renderer
|
|
48
|
+
* Used as fallback when field has no .cell defined
|
|
49
|
+
*/
|
|
50
|
+
function DefaultCell({ value }) {
|
|
51
|
+
if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
|
|
52
|
+
className: "text-muted-foreground",
|
|
53
|
+
children: "-"
|
|
54
|
+
});
|
|
55
|
+
return /* @__PURE__ */ jsx("span", { children: String(value) });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Text cell - simple text display with truncation
|
|
59
|
+
*/
|
|
60
|
+
function TextCell({ value }) {
|
|
61
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
|
|
62
|
+
className: "text-muted-foreground",
|
|
63
|
+
children: "-"
|
|
64
|
+
});
|
|
65
|
+
const text = String(value);
|
|
66
|
+
return /* @__PURE__ */ jsx("span", {
|
|
67
|
+
className: "truncate max-w-[300px] block",
|
|
68
|
+
title: text,
|
|
69
|
+
children: text
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function stripHtmlTags(value) {
|
|
73
|
+
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
74
|
+
}
|
|
75
|
+
function extractTextFromNode(node) {
|
|
76
|
+
if (!node) return "";
|
|
77
|
+
if (Array.isArray(node)) return node.map(extractTextFromNode).filter(Boolean).join(" ");
|
|
78
|
+
if (typeof node.text === "string") return node.text;
|
|
79
|
+
if (Array.isArray(node.content)) return node.content.map(extractTextFromNode).filter(Boolean).join(" ");
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
function getRichTextPreview(value) {
|
|
83
|
+
if (value === null || value === void 0) return "";
|
|
84
|
+
if (typeof value === "string") return /<\/?[a-z][\s\S]*>/i.test(value) ? stripHtmlTags(value) : value.trim();
|
|
85
|
+
if (typeof value === "object") return extractTextFromNode(value).replace(/\s+/g, " ").trim();
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
function RichTextCell({ value }) {
|
|
89
|
+
const text = getRichTextPreview(value);
|
|
90
|
+
if (!text) return /* @__PURE__ */ jsx("span", {
|
|
91
|
+
className: "text-muted-foreground",
|
|
92
|
+
children: "-"
|
|
93
|
+
});
|
|
94
|
+
return /* @__PURE__ */ jsx("span", {
|
|
95
|
+
className: "truncate max-w-[300px] block",
|
|
96
|
+
title: text,
|
|
97
|
+
children: text
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Number cell - right-aligned number display
|
|
102
|
+
*/
|
|
103
|
+
function NumberCell({ value }) {
|
|
104
|
+
if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
|
|
105
|
+
className: "text-muted-foreground",
|
|
106
|
+
children: "-"
|
|
107
|
+
});
|
|
108
|
+
const num = Number(value);
|
|
109
|
+
return /* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "tabular-nums",
|
|
111
|
+
children: Number.isNaN(num) ? String(value) : num.toLocaleString()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Boolean cell - badge display
|
|
116
|
+
*/
|
|
117
|
+
function BooleanCell({ value }) {
|
|
118
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
119
|
+
variant: value ? "default" : "secondary",
|
|
120
|
+
children: value ? "Yes" : "No"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Date cell - formatted date display
|
|
125
|
+
*/
|
|
126
|
+
function DateCell({ value }) {
|
|
127
|
+
if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
|
|
128
|
+
className: "text-muted-foreground",
|
|
129
|
+
children: "-"
|
|
130
|
+
});
|
|
131
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
132
|
+
if (Number.isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", {
|
|
133
|
+
className: "text-muted-foreground",
|
|
134
|
+
children: String(value)
|
|
135
|
+
});
|
|
136
|
+
return /* @__PURE__ */ jsx("span", {
|
|
137
|
+
className: "tabular-nums",
|
|
138
|
+
children: date.toLocaleDateString()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* DateTime cell - formatted date and time display
|
|
143
|
+
*/
|
|
144
|
+
function DateTimeCell({ value }) {
|
|
145
|
+
if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
|
|
146
|
+
className: "text-muted-foreground",
|
|
147
|
+
children: "-"
|
|
148
|
+
});
|
|
149
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
150
|
+
if (Number.isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", {
|
|
151
|
+
className: "text-muted-foreground",
|
|
152
|
+
children: String(value)
|
|
153
|
+
});
|
|
154
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
155
|
+
className: "tabular-nums",
|
|
156
|
+
children: [
|
|
157
|
+
date.toLocaleDateString(),
|
|
158
|
+
" ",
|
|
159
|
+
date.toLocaleTimeString()
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Time cell - formatted time display (for time-only values stored as string)
|
|
165
|
+
*/
|
|
166
|
+
function TimeCell({ value }) {
|
|
167
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
|
|
168
|
+
className: "text-muted-foreground",
|
|
169
|
+
children: "-"
|
|
170
|
+
});
|
|
171
|
+
return /* @__PURE__ */ jsx("span", {
|
|
172
|
+
className: "tabular-nums",
|
|
173
|
+
children: String(value)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Email cell - displays email with mailto link styling
|
|
178
|
+
*/
|
|
179
|
+
function EmailCell({ value }) {
|
|
180
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
|
|
181
|
+
className: "text-muted-foreground",
|
|
182
|
+
children: "-"
|
|
183
|
+
});
|
|
184
|
+
return /* @__PURE__ */ jsx("span", {
|
|
185
|
+
className: "text-primary",
|
|
186
|
+
children: String(value)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Select/Status cell - badge display
|
|
191
|
+
*/
|
|
192
|
+
function SelectCell({ value }) {
|
|
193
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
|
|
194
|
+
className: "text-muted-foreground",
|
|
195
|
+
children: "-"
|
|
196
|
+
});
|
|
197
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
198
|
+
variant: "outline",
|
|
199
|
+
children: String(value)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/client/components/ui/accordion.tsx
|
|
205
|
+
function Accordion$1({ className, ...props }) {
|
|
206
|
+
return /* @__PURE__ */ jsx(Accordion.Root, {
|
|
207
|
+
"data-slot": "accordion",
|
|
208
|
+
className: cn("overflow-hidden border border-border/60 bg-card/30 backdrop-blur-md flex w-full flex-col", className),
|
|
209
|
+
...props
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function AccordionItem({ className, ...props }) {
|
|
213
|
+
return /* @__PURE__ */ jsx(Accordion.Item, {
|
|
214
|
+
"data-slot": "accordion-item",
|
|
215
|
+
className: cn("data-open:bg-muted/30 data-open:backdrop-blur-sm not-last:border-b border-border/40 transition-colors", className),
|
|
216
|
+
...props
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function AccordionTrigger({ className, children, ...props }) {
|
|
220
|
+
return /* @__PURE__ */ jsx(Accordion.Header, {
|
|
221
|
+
className: "flex",
|
|
222
|
+
children: /* @__PURE__ */ jsxs(Accordion.Trigger, {
|
|
223
|
+
"data-slot": "accordion-trigger",
|
|
224
|
+
className: cn("**:data-[slot=accordion-trigger-icon]:text-muted-foreground gap-4 px-4 py-3 text-left text-sm font-medium hover:bg-muted/20 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-center justify-between transition-all outline-none disabled:pointer-events-none disabled:opacity-50", className),
|
|
225
|
+
...props,
|
|
226
|
+
children: [children, /* @__PURE__ */ jsx(CaretDownIcon, {
|
|
227
|
+
"data-slot": "accordion-trigger-icon",
|
|
228
|
+
className: "pointer-events-none shrink-0 transition-transform duration-200 group-aria-expanded/accordion-trigger:rotate-180"
|
|
229
|
+
})]
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function AccordionContent({ className, children, ...props }) {
|
|
234
|
+
return /* @__PURE__ */ jsx(Accordion.Panel, {
|
|
235
|
+
"data-slot": "accordion-content",
|
|
236
|
+
className: "data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden",
|
|
237
|
+
...props,
|
|
238
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
239
|
+
className: cn("px-4 pt-0 pb-4 text-sm [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", className),
|
|
240
|
+
children
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/client/components/ui/tabs.tsx
|
|
247
|
+
function Tabs$1({ className, orientation = "horizontal", ...props }) {
|
|
248
|
+
return /* @__PURE__ */ jsx(Tabs.Root, {
|
|
249
|
+
"data-slot": "tabs",
|
|
250
|
+
"data-orientation": orientation,
|
|
251
|
+
className: cn("gap-2 group/tabs flex data-[orientation=horizontal]:flex-col", className),
|
|
252
|
+
...props
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const tabsListVariants = cva("p-[3px] group-data-horizontal/tabs:h-8 group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", {
|
|
256
|
+
variants: { variant: {
|
|
257
|
+
default: "bg-muted",
|
|
258
|
+
line: "gap-1 bg-transparent"
|
|
259
|
+
} },
|
|
260
|
+
defaultVariants: { variant: "default" }
|
|
261
|
+
});
|
|
262
|
+
function TabsList({ className, variant = "default", ...props }) {
|
|
263
|
+
return /* @__PURE__ */ jsx(Tabs.List, {
|
|
264
|
+
"data-slot": "tabs-list",
|
|
265
|
+
"data-variant": variant,
|
|
266
|
+
className: cn(tabsListVariants({ variant }), className),
|
|
267
|
+
...props
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function TabsTrigger({ className, ...props }) {
|
|
271
|
+
return /* @__PURE__ */ jsx(Tabs.Tab, {
|
|
272
|
+
"data-slot": "tabs-trigger",
|
|
273
|
+
className: cn("gap-1.5 border border-transparent px-1.5 py-0.5 text-xs font-medium group-data-vertical/tabs:py-[calc(--spacing(1.25))] [&_svg:not([class*='size-'])]:size-3.5 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className),
|
|
274
|
+
...props
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function TabsContent({ className, ...props }) {
|
|
278
|
+
return /* @__PURE__ */ jsx(Tabs.Panel, {
|
|
279
|
+
"data-slot": "tabs-content",
|
|
280
|
+
className: cn("text-xs/relaxed flex-1 outline-none", className),
|
|
281
|
+
...props
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/client/hooks/use-collection.ts
|
|
287
|
+
/**
|
|
288
|
+
* Hook to fetch collection list with filters, sorting, pagination
|
|
289
|
+
*
|
|
290
|
+
* Uses RegisteredCMS from module augmentation for automatic type inference.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```tsx
|
|
294
|
+
* // Types inferred from module augmentation!
|
|
295
|
+
* const { data } = useCollectionList("barbers");
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
function useCollectionList(collection, options, queryOptions) {
|
|
299
|
+
const client = useAdminStore(selectClient);
|
|
300
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
301
|
+
return useQuery({
|
|
302
|
+
...createQuestpieQueryOptions(client, {
|
|
303
|
+
keyPrefix: ["questpie", "collections"],
|
|
304
|
+
locale: contentLocale
|
|
305
|
+
}).collections[collection].find({
|
|
306
|
+
...options,
|
|
307
|
+
locale: contentLocale
|
|
308
|
+
}),
|
|
309
|
+
...queryOptions
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Hook to count collection items with optional filters
|
|
314
|
+
*
|
|
315
|
+
* More efficient than useCollectionList when you only need the count.
|
|
316
|
+
* Uses dedicated count endpoint that doesn't fetch actual documents.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```tsx
|
|
320
|
+
* // Count all items
|
|
321
|
+
* const { data: count } = useCollectionCount("barbers");
|
|
322
|
+
*
|
|
323
|
+
* // Count with filter
|
|
324
|
+
* const { data: count } = useCollectionCount("appointments", {
|
|
325
|
+
* where: { status: "pending" }
|
|
326
|
+
* });
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
function useCollectionCount(collection, options, queryOptions) {
|
|
330
|
+
const client = useAdminStore(selectClient);
|
|
331
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
332
|
+
return useQuery({
|
|
333
|
+
...createQuestpieQueryOptions(client, {
|
|
334
|
+
keyPrefix: ["questpie", "collections"],
|
|
335
|
+
locale: contentLocale
|
|
336
|
+
}).collections[collection].count({
|
|
337
|
+
...options,
|
|
338
|
+
locale: contentLocale
|
|
339
|
+
}),
|
|
340
|
+
...queryOptions
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Hook to fetch single collection item
|
|
345
|
+
*
|
|
346
|
+
* Uses RegisteredCMS from module augmentation for automatic type inference.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```tsx
|
|
350
|
+
* // Types inferred from module augmentation!
|
|
351
|
+
* const { data } = useCollectionItem("barbers", "123");
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
function useCollectionItem(collection, id, options, queryOptions) {
|
|
355
|
+
const client = useAdminStore(selectClient);
|
|
356
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
357
|
+
return useQuery({
|
|
358
|
+
...createQuestpieQueryOptions(client, {
|
|
359
|
+
keyPrefix: ["questpie", "collections"],
|
|
360
|
+
locale: contentLocale
|
|
361
|
+
}).collections[collection].findOne({
|
|
362
|
+
where: { id },
|
|
363
|
+
locale: contentLocale,
|
|
364
|
+
...options
|
|
365
|
+
}),
|
|
366
|
+
...queryOptions
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Hook to create collection item
|
|
371
|
+
*
|
|
372
|
+
* Uses RegisteredCMS from module augmentation for automatic type inference.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```tsx
|
|
376
|
+
* // Types inferred from module augmentation!
|
|
377
|
+
* const { mutate } = useCollectionCreate("barbers");
|
|
378
|
+
* mutate({ name: "John", ... });
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
function useCollectionCreate(collection, mutationOptions) {
|
|
382
|
+
const client = useAdminStore(selectClient);
|
|
383
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
384
|
+
const queryClient = useQueryClient();
|
|
385
|
+
const queryOpts = createQuestpieQueryOptions(client, {
|
|
386
|
+
keyPrefix: ["questpie", "collections"],
|
|
387
|
+
locale: contentLocale
|
|
388
|
+
});
|
|
389
|
+
const baseOptions = queryOpts.collections[collection].create();
|
|
390
|
+
const listQueryKey = queryOpts.key([
|
|
391
|
+
"collections",
|
|
392
|
+
collection,
|
|
393
|
+
"find",
|
|
394
|
+
contentLocale
|
|
395
|
+
]);
|
|
396
|
+
const countQueryKey = queryOpts.key([
|
|
397
|
+
"collections",
|
|
398
|
+
collection,
|
|
399
|
+
"count",
|
|
400
|
+
contentLocale
|
|
401
|
+
]);
|
|
402
|
+
return useMutation({
|
|
403
|
+
...baseOptions,
|
|
404
|
+
onSuccess: (data, variables, context) => {
|
|
405
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
406
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
407
|
+
(mutationOptions?.onSuccess)?.(data, variables, context);
|
|
408
|
+
},
|
|
409
|
+
onSettled: (data, error, variables, context) => {
|
|
410
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
411
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
412
|
+
(mutationOptions?.onSettled)?.(data, error, variables, context);
|
|
413
|
+
},
|
|
414
|
+
...mutationOptions
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Hook to update collection item
|
|
419
|
+
*
|
|
420
|
+
* Uses RegisteredCMS from module augmentation for automatic type inference.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```tsx
|
|
424
|
+
* // Types inferred from module augmentation!
|
|
425
|
+
* const { mutate } = useCollectionUpdate("barbers");
|
|
426
|
+
* mutate({ id: "123", data: { name: "John" } });
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
function useCollectionUpdate(collection, mutationOptions) {
|
|
430
|
+
const client = useAdminStore(selectClient);
|
|
431
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
432
|
+
const queryClient = useQueryClient();
|
|
433
|
+
const queryOpts = createQuestpieQueryOptions(client, {
|
|
434
|
+
keyPrefix: ["questpie", "collections"],
|
|
435
|
+
locale: contentLocale
|
|
436
|
+
});
|
|
437
|
+
const baseOptions = queryOpts.collections[collection].update();
|
|
438
|
+
const listQueryKey = queryOpts.key([
|
|
439
|
+
"collections",
|
|
440
|
+
collection,
|
|
441
|
+
"find",
|
|
442
|
+
contentLocale
|
|
443
|
+
]);
|
|
444
|
+
const countQueryKey = queryOpts.key([
|
|
445
|
+
"collections",
|
|
446
|
+
collection,
|
|
447
|
+
"count",
|
|
448
|
+
contentLocale
|
|
449
|
+
]);
|
|
450
|
+
const itemQueryKey = queryOpts.key([
|
|
451
|
+
"collections",
|
|
452
|
+
collection,
|
|
453
|
+
"findOne",
|
|
454
|
+
contentLocale
|
|
455
|
+
]);
|
|
456
|
+
return useMutation({
|
|
457
|
+
...baseOptions,
|
|
458
|
+
onSuccess: (data, variables, context) => {
|
|
459
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
460
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
461
|
+
queryClient.invalidateQueries({ queryKey: itemQueryKey });
|
|
462
|
+
(mutationOptions?.onSuccess)?.(data, variables, context);
|
|
463
|
+
},
|
|
464
|
+
onSettled: (data, error, variables, context) => {
|
|
465
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
466
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
467
|
+
queryClient.invalidateQueries({ queryKey: itemQueryKey });
|
|
468
|
+
(mutationOptions?.onSettled)?.(data, error, variables, context);
|
|
469
|
+
},
|
|
470
|
+
...mutationOptions
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Hook to delete collection item
|
|
475
|
+
*
|
|
476
|
+
* Uses RegisteredCMS from module augmentation for automatic type inference.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```tsx
|
|
480
|
+
* // Types inferred from module augmentation!
|
|
481
|
+
* const { mutate } = useCollectionDelete("barbers");
|
|
482
|
+
* mutate("123");
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
function useCollectionDelete(collection, mutationOptions) {
|
|
486
|
+
const client = useAdminStore(selectClient);
|
|
487
|
+
const { locale: contentLocale } = useScopedLocale();
|
|
488
|
+
const queryClient = useQueryClient();
|
|
489
|
+
const queryOpts = createQuestpieQueryOptions(client, {
|
|
490
|
+
keyPrefix: ["questpie", "collections"],
|
|
491
|
+
locale: contentLocale
|
|
492
|
+
});
|
|
493
|
+
const baseOptions = queryOpts.collections[collection].delete();
|
|
494
|
+
const listQueryKey = queryOpts.key([
|
|
495
|
+
"collections",
|
|
496
|
+
collection,
|
|
497
|
+
"find",
|
|
498
|
+
contentLocale
|
|
499
|
+
]);
|
|
500
|
+
const countQueryKey = queryOpts.key([
|
|
501
|
+
"collections",
|
|
502
|
+
collection,
|
|
503
|
+
"count",
|
|
504
|
+
contentLocale
|
|
505
|
+
]);
|
|
506
|
+
const itemQueryKey = queryOpts.key([
|
|
507
|
+
"collections",
|
|
508
|
+
collection,
|
|
509
|
+
"findOne",
|
|
510
|
+
contentLocale
|
|
511
|
+
]);
|
|
512
|
+
return useMutation({
|
|
513
|
+
...baseOptions,
|
|
514
|
+
onSuccess: (data, variables, context) => {
|
|
515
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
516
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
517
|
+
queryClient.invalidateQueries({ queryKey: itemQueryKey });
|
|
518
|
+
(mutationOptions?.onSuccess)?.(data, variables, context);
|
|
519
|
+
},
|
|
520
|
+
onSettled: (data, error, variables, context) => {
|
|
521
|
+
queryClient.invalidateQueries({ queryKey: listQueryKey });
|
|
522
|
+
queryClient.invalidateQueries({ queryKey: countQueryKey });
|
|
523
|
+
queryClient.invalidateQueries({ queryKey: itemQueryKey });
|
|
524
|
+
(mutationOptions?.onSettled)?.(data, error, variables, context);
|
|
525
|
+
},
|
|
526
|
+
...mutationOptions
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/client/components/ui/skeleton.tsx
|
|
532
|
+
function Skeleton({ className, ...props }) {
|
|
533
|
+
return /* @__PURE__ */ jsx("div", {
|
|
534
|
+
"data-slot": "skeleton",
|
|
535
|
+
className: cn("bg-muted animate-pulse", className),
|
|
536
|
+
...props
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/client/components/error-boundary.tsx
|
|
542
|
+
/**
|
|
543
|
+
* ErrorBoundary - Catches JavaScript errors in child component tree
|
|
544
|
+
*
|
|
545
|
+
* Prevents entire UI from crashing when a component throws an error.
|
|
546
|
+
* Use around widgets, sections, or any component that might fail.
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```tsx
|
|
550
|
+
* <ErrorBoundary fallback={<WidgetError />}>
|
|
551
|
+
* <ChartWidget config={config} />
|
|
552
|
+
* </ErrorBoundary>
|
|
553
|
+
*
|
|
554
|
+
* // With error callback
|
|
555
|
+
* <ErrorBoundary
|
|
556
|
+
* fallback={(error) => <p>Error: {error.message}</p>}
|
|
557
|
+
* onError={(error) => logToService(error)}
|
|
558
|
+
* >
|
|
559
|
+
* <DashboardWidget config={config} />
|
|
560
|
+
* </ErrorBoundary>
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
563
|
+
var ErrorBoundary = class extends React$1.Component {
|
|
564
|
+
constructor(props) {
|
|
565
|
+
super(props);
|
|
566
|
+
this.state = {
|
|
567
|
+
hasError: false,
|
|
568
|
+
error: null
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
static getDerivedStateFromError(error) {
|
|
572
|
+
return {
|
|
573
|
+
hasError: true,
|
|
574
|
+
error
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
componentDidCatch(error, errorInfo) {
|
|
578
|
+
this.props.onError?.(error, errorInfo);
|
|
579
|
+
}
|
|
580
|
+
render() {
|
|
581
|
+
if (this.state.hasError && this.state.error) {
|
|
582
|
+
const { fallback } = this.props;
|
|
583
|
+
if (typeof fallback === "function") return fallback(this.state.error);
|
|
584
|
+
if (fallback) return fallback;
|
|
585
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
586
|
+
className: "rounded-lg border border-destructive/20 bg-destructive/5 p-4",
|
|
587
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
588
|
+
className: "text-sm font-medium text-destructive",
|
|
589
|
+
children: "Something went wrong"
|
|
590
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
591
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
592
|
+
children: this.state.error.message
|
|
593
|
+
})]
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return this.props.children;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
/**
|
|
600
|
+
* WidgetErrorBoundary - Specialized error boundary for dashboard widgets
|
|
601
|
+
*/
|
|
602
|
+
function WidgetErrorBoundary({ children, widgetType }) {
|
|
603
|
+
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
604
|
+
fallback: (error) => /* @__PURE__ */ jsxs("div", {
|
|
605
|
+
className: "rounded-lg border border-destructive/20 bg-destructive/5 p-4",
|
|
606
|
+
children: [
|
|
607
|
+
/* @__PURE__ */ jsx("p", {
|
|
608
|
+
className: "text-sm font-medium text-destructive",
|
|
609
|
+
children: "Widget Error"
|
|
610
|
+
}),
|
|
611
|
+
widgetType && /* @__PURE__ */ jsxs("p", {
|
|
612
|
+
className: "text-xs text-muted-foreground",
|
|
613
|
+
children: ["Type: ", widgetType]
|
|
614
|
+
}),
|
|
615
|
+
/* @__PURE__ */ jsx("p", {
|
|
616
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
617
|
+
children: error.message
|
|
618
|
+
})
|
|
619
|
+
]
|
|
620
|
+
}),
|
|
621
|
+
children
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/client/views/dashboard/widget-card.tsx
|
|
627
|
+
/**
|
|
628
|
+
* WidgetCard Component
|
|
629
|
+
*
|
|
630
|
+
* Standardized card wrapper for dashboard widgets with multiple variants.
|
|
631
|
+
* Provides consistent styling, header actions, and loading/error states.
|
|
632
|
+
*/
|
|
633
|
+
const variantStyles$2 = {
|
|
634
|
+
default: "",
|
|
635
|
+
compact: "py-3 gap-3",
|
|
636
|
+
featured: "border-primary/30 bg-gradient-to-br from-primary/5 to-transparent shadow-sm"
|
|
637
|
+
};
|
|
638
|
+
const variantContentStyles = {
|
|
639
|
+
default: "",
|
|
640
|
+
compact: "pt-0",
|
|
641
|
+
featured: ""
|
|
642
|
+
};
|
|
643
|
+
function WidgetCardLoading({ variant = "default" }) {
|
|
644
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
645
|
+
className: cn("h-full flex flex-col", variantStyles$2[variant]),
|
|
646
|
+
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }), /* @__PURE__ */ jsx(CardContent, {
|
|
647
|
+
className: cn("flex-1", variantContentStyles[variant]),
|
|
648
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-20 w-full" })
|
|
649
|
+
})]
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function WidgetCardError({ error, variant = "default", onRetry }) {
|
|
653
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
654
|
+
className: cn("h-full flex flex-col border-destructive/20 bg-destructive/5", variantStyles$2[variant]),
|
|
655
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, {
|
|
656
|
+
className: "text-sm font-medium text-destructive",
|
|
657
|
+
children: "Error"
|
|
658
|
+
}), onRetry && /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Button, {
|
|
659
|
+
variant: "ghost",
|
|
660
|
+
size: "icon-xs",
|
|
661
|
+
onClick: onRetry,
|
|
662
|
+
children: /* @__PURE__ */ jsx(ArrowClockwise, { className: "h-3.5 w-3.5" })
|
|
663
|
+
}) })] }), /* @__PURE__ */ jsx(CardContent, {
|
|
664
|
+
className: cn("flex-1", variantContentStyles[variant]),
|
|
665
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
666
|
+
className: "text-xs text-muted-foreground",
|
|
667
|
+
children: error.message
|
|
668
|
+
})
|
|
669
|
+
})]
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* WidgetCard - Standardized card wrapper for dashboard widgets
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```tsx
|
|
677
|
+
* <WidgetCard
|
|
678
|
+
* title="Revenue"
|
|
679
|
+
* description="Monthly sales total"
|
|
680
|
+
* variant="featured"
|
|
681
|
+
* onRefresh={() => refetch()}
|
|
682
|
+
* >
|
|
683
|
+
* <div className="text-2xl font-bold">$12,345</div>
|
|
684
|
+
* </WidgetCard>
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
function WidgetCard({ title, description, icon: Icon, variant = "default", isLoading, isRefreshing, error, onRefresh, onExpand, actions, className, loadingSkeleton, children }) {
|
|
688
|
+
if (isLoading) {
|
|
689
|
+
if (loadingSkeleton) return /* @__PURE__ */ jsxs(Card, {
|
|
690
|
+
className: cn("h-full flex flex-col", variantStyles$2[variant], className),
|
|
691
|
+
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }), /* @__PURE__ */ jsx(CardContent, {
|
|
692
|
+
className: cn("flex-1", variantContentStyles[variant]),
|
|
693
|
+
children: loadingSkeleton
|
|
694
|
+
})]
|
|
695
|
+
});
|
|
696
|
+
return /* @__PURE__ */ jsx(WidgetCardLoading, { variant });
|
|
697
|
+
}
|
|
698
|
+
if (error) return /* @__PURE__ */ jsx(WidgetCardError, {
|
|
699
|
+
error,
|
|
700
|
+
variant,
|
|
701
|
+
onRetry: onRefresh
|
|
702
|
+
});
|
|
703
|
+
const hasHeader = title || description || Icon || onRefresh || onExpand || actions?.length;
|
|
704
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
705
|
+
className: cn("h-full flex flex-col", variantStyles$2[variant], className),
|
|
706
|
+
children: [hasHeader && /* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsxs("div", {
|
|
707
|
+
className: "flex items-center gap-2",
|
|
708
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ jsxs("div", {
|
|
709
|
+
className: "flex-1 min-w-0",
|
|
710
|
+
children: [title && /* @__PURE__ */ jsx(CardTitle, {
|
|
711
|
+
className: "text-sm font-medium truncate",
|
|
712
|
+
children: title
|
|
713
|
+
}), description && /* @__PURE__ */ jsx(CardDescription, {
|
|
714
|
+
className: "truncate",
|
|
715
|
+
children: description
|
|
716
|
+
})]
|
|
717
|
+
})]
|
|
718
|
+
}), (onRefresh || onExpand || actions?.length) && /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsxs("div", {
|
|
719
|
+
className: "flex items-center gap-1",
|
|
720
|
+
children: [
|
|
721
|
+
actions?.map((action) => /* @__PURE__ */ jsx(Button, {
|
|
722
|
+
variant: "ghost",
|
|
723
|
+
size: "icon-xs",
|
|
724
|
+
onClick: action.onClick,
|
|
725
|
+
title: action.label,
|
|
726
|
+
children: action.icon && typeof action.icon !== "string" && /* @__PURE__ */ jsx(action.icon, { className: "h-3.5 w-3.5" })
|
|
727
|
+
}, action.id)),
|
|
728
|
+
onRefresh && /* @__PURE__ */ jsx(Button, {
|
|
729
|
+
variant: "ghost",
|
|
730
|
+
size: "icon-xs",
|
|
731
|
+
onClick: onRefresh,
|
|
732
|
+
title: "Refresh",
|
|
733
|
+
disabled: isRefreshing,
|
|
734
|
+
children: /* @__PURE__ */ jsx(ArrowClockwise, { className: cn("h-3.5 w-3.5", isRefreshing && "animate-spin") })
|
|
735
|
+
}),
|
|
736
|
+
onExpand && /* @__PURE__ */ jsx(Button, {
|
|
737
|
+
variant: "ghost",
|
|
738
|
+
size: "icon-xs",
|
|
739
|
+
onClick: onExpand,
|
|
740
|
+
title: "Expand",
|
|
741
|
+
children: /* @__PURE__ */ jsx(ArrowsOutSimple, { className: "h-3.5 w-3.5" })
|
|
742
|
+
})
|
|
743
|
+
]
|
|
744
|
+
}) })] }), /* @__PURE__ */ jsx(CardContent, {
|
|
745
|
+
className: cn("flex-1", variantContentStyles[variant], !hasHeader && "pt-0"),
|
|
746
|
+
children
|
|
747
|
+
})]
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/client/components/widgets/widget-skeletons.tsx
|
|
753
|
+
/**
|
|
754
|
+
* Widget Skeletons
|
|
755
|
+
*
|
|
756
|
+
* Loading skeleton components for different widget types.
|
|
757
|
+
* Provides visual feedback while data is being fetched.
|
|
758
|
+
*/
|
|
759
|
+
function StatsWidgetSkeleton() {
|
|
760
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
761
|
+
className: "space-y-3",
|
|
762
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" })]
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function ChartWidgetSkeleton() {
|
|
766
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
767
|
+
className: "h-48 w-full flex items-end gap-2 pt-4",
|
|
768
|
+
children: [
|
|
769
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[40%]" }),
|
|
770
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[65%]" }),
|
|
771
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[45%]" }),
|
|
772
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[80%]" }),
|
|
773
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[55%]" }),
|
|
774
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[70%]" }),
|
|
775
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[50%]" }),
|
|
776
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[75%]" }),
|
|
777
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[60%]" }),
|
|
778
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[85%]" })
|
|
779
|
+
]
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function RecentItemsWidgetSkeleton({ count = 5 }) {
|
|
783
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
784
|
+
className: "space-y-1",
|
|
785
|
+
children: [
|
|
786
|
+
/* @__PURE__ */ jsx(SkeletonRow, {}),
|
|
787
|
+
count > 1 && /* @__PURE__ */ jsx(SkeletonRow, {}),
|
|
788
|
+
count > 2 && /* @__PURE__ */ jsx(SkeletonRow, {}),
|
|
789
|
+
count > 3 && /* @__PURE__ */ jsx(SkeletonRow, {}),
|
|
790
|
+
count > 4 && /* @__PURE__ */ jsx(SkeletonRow, {})
|
|
791
|
+
]
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function SkeletonRow() {
|
|
795
|
+
return /* @__PURE__ */ jsx("div", {
|
|
796
|
+
className: "flex items-center gap-3 p-2",
|
|
797
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
798
|
+
className: "flex-1 space-y-2",
|
|
799
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-1/2" })]
|
|
800
|
+
})
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
function TableWidgetSkeleton({ rows = 5, columns = 3 }) {
|
|
804
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
805
|
+
className: "-mx-5",
|
|
806
|
+
children: [
|
|
807
|
+
/* @__PURE__ */ jsxs("div", {
|
|
808
|
+
className: "flex gap-4 px-5 py-2 border-b border-border/50",
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }),
|
|
811
|
+
columns > 1 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }),
|
|
812
|
+
columns > 2 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" })
|
|
813
|
+
]
|
|
814
|
+
}),
|
|
815
|
+
/* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
|
|
816
|
+
rows > 1 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
|
|
817
|
+
rows > 2 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
|
|
818
|
+
rows > 3 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
|
|
819
|
+
rows > 4 && /* @__PURE__ */ jsx(TableSkeletonRow, {
|
|
820
|
+
columns,
|
|
821
|
+
last: true
|
|
822
|
+
})
|
|
823
|
+
]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function TableSkeletonRow({ columns = 3, last = false }) {
|
|
827
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
828
|
+
className: cn("flex gap-4 px-5 py-3 border-b border-border/30", last && "border-0"),
|
|
829
|
+
children: [
|
|
830
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" }),
|
|
831
|
+
columns > 1 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }),
|
|
832
|
+
columns > 2 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" })
|
|
833
|
+
]
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function TimelineWidgetSkeleton({ count = 5 }) {
|
|
837
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
838
|
+
className: "space-y-4",
|
|
839
|
+
children: [
|
|
840
|
+
/* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
|
|
841
|
+
count > 1 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
|
|
842
|
+
count > 2 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
|
|
843
|
+
count > 3 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
|
|
844
|
+
count > 4 && /* @__PURE__ */ jsx(TimelineSkeletonItem, { last: true })
|
|
845
|
+
]
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
function TimelineSkeletonItem({ last = false }) {
|
|
849
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
850
|
+
className: "flex gap-3",
|
|
851
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
852
|
+
className: "flex flex-col items-center",
|
|
853
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-3 rounded-full" }), !last && /* @__PURE__ */ jsx(Skeleton, { className: "w-0.5 flex-1 mt-1" })]
|
|
854
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
855
|
+
className: "flex-1 space-y-1 pb-4",
|
|
856
|
+
children: [
|
|
857
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
|
|
858
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-1/2" }),
|
|
859
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-20" })
|
|
860
|
+
]
|
|
861
|
+
})]
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
function ProgressWidgetSkeleton() {
|
|
865
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
866
|
+
className: "space-y-3",
|
|
867
|
+
children: [
|
|
868
|
+
/* @__PURE__ */ jsxs("div", {
|
|
869
|
+
className: "flex justify-between items-center",
|
|
870
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-12" })]
|
|
871
|
+
}),
|
|
872
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-2 w-full rounded-full" }),
|
|
873
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-24" })
|
|
874
|
+
]
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function ValueWidgetSkeleton({ featured = false }) {
|
|
878
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
879
|
+
className: "space-y-3",
|
|
880
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
881
|
+
className: "flex items-start gap-3",
|
|
882
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-10 rounded-md shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
883
|
+
className: "flex-1 space-y-2",
|
|
884
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }), /* @__PURE__ */ jsx(Skeleton, { className: cn("h-8", featured ? "w-40" : "w-24") })]
|
|
885
|
+
})]
|
|
886
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-32" })]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/client/components/widgets/chart-widget.tsx
|
|
892
|
+
/**
|
|
893
|
+
* Chart Widget
|
|
894
|
+
*
|
|
895
|
+
* Displays data visualization over time.
|
|
896
|
+
* Uses WidgetCard for consistent styling.
|
|
897
|
+
*/
|
|
898
|
+
const CHART_COLORS = [
|
|
899
|
+
"var(--color-chart-1)",
|
|
900
|
+
"var(--color-chart-2)",
|
|
901
|
+
"var(--color-chart-3)",
|
|
902
|
+
"var(--color-chart-4)",
|
|
903
|
+
"var(--color-chart-5)"
|
|
904
|
+
];
|
|
905
|
+
/**
|
|
906
|
+
* Custom tooltip component matching shadcn style
|
|
907
|
+
*/
|
|
908
|
+
function ChartTooltip({ active, payload, label }) {
|
|
909
|
+
if (!active || !payload?.length) return null;
|
|
910
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
911
|
+
className: "rounded-md border border-border bg-background px-3 py-2 text-xs shadow-md",
|
|
912
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
913
|
+
className: "font-medium text-foreground",
|
|
914
|
+
children: label
|
|
915
|
+
}), payload.map((entry) => /* @__PURE__ */ jsxs("p", {
|
|
916
|
+
className: "text-muted-foreground",
|
|
917
|
+
children: [
|
|
918
|
+
entry.name,
|
|
919
|
+
":",
|
|
920
|
+
" ",
|
|
921
|
+
/* @__PURE__ */ jsx("span", {
|
|
922
|
+
className: "font-medium text-foreground",
|
|
923
|
+
children: entry.value
|
|
924
|
+
})
|
|
925
|
+
]
|
|
926
|
+
}, String(entry.name)))]
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Chart widget component
|
|
931
|
+
*
|
|
932
|
+
* Displays:
|
|
933
|
+
* - Time series data visualization
|
|
934
|
+
* - Multiple chart types (line, bar, area, pie)
|
|
935
|
+
* - Configurable time ranges
|
|
936
|
+
*/
|
|
937
|
+
function ChartWidget({ config }) {
|
|
938
|
+
const resolveText = useResolveText();
|
|
939
|
+
const { collection, field, chartType = "area", timeRange = "30d", label, color = "var(--color-chart-1)", showGrid = true } = config;
|
|
940
|
+
const { data, isLoading, error, refetch } = useCollectionList(collection, { limit: 1e3 });
|
|
941
|
+
const items = Array.isArray(data?.docs) ? data.docs : [];
|
|
942
|
+
const displayLabel = label ? resolveText(label) : `${formatCollectionName(collection)} by ${field}`;
|
|
943
|
+
const chartData = React$1.useMemo(() => {
|
|
944
|
+
if (!items.length) return [];
|
|
945
|
+
const grouped = items.reduce((acc, item) => {
|
|
946
|
+
const value = item[field];
|
|
947
|
+
if (value === void 0 || value === null) return acc;
|
|
948
|
+
let key;
|
|
949
|
+
if (value instanceof Date || !isNaN(Date.parse(value))) key = formatDateForRange(new Date(value), timeRange);
|
|
950
|
+
else key = String(value);
|
|
951
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
952
|
+
return acc;
|
|
953
|
+
}, {});
|
|
954
|
+
return Object.entries(grouped).map(([name, value]) => ({
|
|
955
|
+
name,
|
|
956
|
+
value
|
|
957
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
958
|
+
}, [
|
|
959
|
+
items,
|
|
960
|
+
field,
|
|
961
|
+
timeRange
|
|
962
|
+
]);
|
|
963
|
+
const emptyContent = /* @__PURE__ */ jsx("div", {
|
|
964
|
+
className: "flex h-48 items-center justify-center text-muted-foreground",
|
|
965
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
966
|
+
className: "text-sm",
|
|
967
|
+
children: "No data available"
|
|
968
|
+
})
|
|
969
|
+
});
|
|
970
|
+
const chartContent = chartData.length === 0 ? emptyContent : /* @__PURE__ */ jsx("div", {
|
|
971
|
+
className: "h-48 w-full",
|
|
972
|
+
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
973
|
+
width: "100%",
|
|
974
|
+
height: "100%",
|
|
975
|
+
children: renderChart(chartType, chartData, color, showGrid)
|
|
976
|
+
})
|
|
977
|
+
});
|
|
978
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
979
|
+
title: displayLabel,
|
|
980
|
+
isLoading,
|
|
981
|
+
loadingSkeleton: /* @__PURE__ */ jsx(ChartWidgetSkeleton, {}),
|
|
982
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
983
|
+
onRefresh: () => refetch(),
|
|
984
|
+
children: chartContent
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Render the appropriate chart type
|
|
989
|
+
*/
|
|
990
|
+
function renderChart(type, data, color, showGrid) {
|
|
991
|
+
const commonProps = {
|
|
992
|
+
data,
|
|
993
|
+
margin: {
|
|
994
|
+
top: 5,
|
|
995
|
+
right: 5,
|
|
996
|
+
left: -20,
|
|
997
|
+
bottom: 5
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const axisStyle = {
|
|
1001
|
+
fontSize: 10,
|
|
1002
|
+
fill: "var(--color-muted-foreground)"
|
|
1003
|
+
};
|
|
1004
|
+
switch (type) {
|
|
1005
|
+
case "line": return /* @__PURE__ */ jsxs(LineChart, {
|
|
1006
|
+
...commonProps,
|
|
1007
|
+
children: [
|
|
1008
|
+
showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
|
|
1009
|
+
strokeDasharray: "3 3",
|
|
1010
|
+
stroke: "var(--color-border)",
|
|
1011
|
+
opacity: .5
|
|
1012
|
+
}),
|
|
1013
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
1014
|
+
dataKey: "name",
|
|
1015
|
+
tick: axisStyle,
|
|
1016
|
+
tickLine: false,
|
|
1017
|
+
axisLine: false
|
|
1018
|
+
}),
|
|
1019
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
1020
|
+
tick: axisStyle,
|
|
1021
|
+
tickLine: false,
|
|
1022
|
+
axisLine: false
|
|
1023
|
+
}),
|
|
1024
|
+
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) }),
|
|
1025
|
+
/* @__PURE__ */ jsx(Line, {
|
|
1026
|
+
type: "monotone",
|
|
1027
|
+
dataKey: "value",
|
|
1028
|
+
stroke: color,
|
|
1029
|
+
strokeWidth: 2,
|
|
1030
|
+
dot: false
|
|
1031
|
+
})
|
|
1032
|
+
]
|
|
1033
|
+
});
|
|
1034
|
+
case "bar": return /* @__PURE__ */ jsxs(BarChart, {
|
|
1035
|
+
...commonProps,
|
|
1036
|
+
children: [
|
|
1037
|
+
showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
|
|
1038
|
+
strokeDasharray: "3 3",
|
|
1039
|
+
stroke: "var(--color-border)",
|
|
1040
|
+
opacity: .5
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
1043
|
+
dataKey: "name",
|
|
1044
|
+
tick: axisStyle,
|
|
1045
|
+
tickLine: false,
|
|
1046
|
+
axisLine: false
|
|
1047
|
+
}),
|
|
1048
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
1049
|
+
tick: axisStyle,
|
|
1050
|
+
tickLine: false,
|
|
1051
|
+
axisLine: false
|
|
1052
|
+
}),
|
|
1053
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
1054
|
+
content: /* @__PURE__ */ jsx(ChartTooltip, {}),
|
|
1055
|
+
cursor: {
|
|
1056
|
+
fill: "var(--color-muted)",
|
|
1057
|
+
opacity: .3
|
|
1058
|
+
}
|
|
1059
|
+
}),
|
|
1060
|
+
/* @__PURE__ */ jsx(Bar, {
|
|
1061
|
+
dataKey: "value",
|
|
1062
|
+
fill: color,
|
|
1063
|
+
radius: [
|
|
1064
|
+
2,
|
|
1065
|
+
2,
|
|
1066
|
+
0,
|
|
1067
|
+
0
|
|
1068
|
+
]
|
|
1069
|
+
})
|
|
1070
|
+
]
|
|
1071
|
+
});
|
|
1072
|
+
case "pie": return /* @__PURE__ */ jsxs(PieChart, { children: [/* @__PURE__ */ jsx(Pie, {
|
|
1073
|
+
data,
|
|
1074
|
+
dataKey: "value",
|
|
1075
|
+
nameKey: "name",
|
|
1076
|
+
cx: "50%",
|
|
1077
|
+
cy: "50%",
|
|
1078
|
+
outerRadius: 60,
|
|
1079
|
+
label: ({ name }) => name,
|
|
1080
|
+
labelLine: false,
|
|
1081
|
+
stroke: "var(--color-background)",
|
|
1082
|
+
strokeWidth: 2,
|
|
1083
|
+
children: data.map((entry) => /* @__PURE__ */ jsx(Cell, { fill: CHART_COLORS[data.indexOf(entry) % CHART_COLORS.length] }, `cell-${entry.name}`))
|
|
1084
|
+
}), /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) })] });
|
|
1085
|
+
case "area":
|
|
1086
|
+
default: return /* @__PURE__ */ jsxs(AreaChart, {
|
|
1087
|
+
...commonProps,
|
|
1088
|
+
children: [
|
|
1089
|
+
showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
|
|
1090
|
+
strokeDasharray: "3 3",
|
|
1091
|
+
stroke: "var(--color-border)",
|
|
1092
|
+
opacity: .5
|
|
1093
|
+
}),
|
|
1094
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
1095
|
+
dataKey: "name",
|
|
1096
|
+
tick: axisStyle,
|
|
1097
|
+
tickLine: false,
|
|
1098
|
+
axisLine: false
|
|
1099
|
+
}),
|
|
1100
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
1101
|
+
tick: axisStyle,
|
|
1102
|
+
tickLine: false,
|
|
1103
|
+
axisLine: false
|
|
1104
|
+
}),
|
|
1105
|
+
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) }),
|
|
1106
|
+
/* @__PURE__ */ jsx(Area, {
|
|
1107
|
+
type: "monotone",
|
|
1108
|
+
dataKey: "value",
|
|
1109
|
+
stroke: color,
|
|
1110
|
+
fill: color,
|
|
1111
|
+
fillOpacity: .15
|
|
1112
|
+
})
|
|
1113
|
+
]
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Format date based on time range for grouping
|
|
1119
|
+
*/
|
|
1120
|
+
function formatDateForRange(date, timeRange) {
|
|
1121
|
+
const months = [
|
|
1122
|
+
"Jan",
|
|
1123
|
+
"Feb",
|
|
1124
|
+
"Mar",
|
|
1125
|
+
"Apr",
|
|
1126
|
+
"May",
|
|
1127
|
+
"Jun",
|
|
1128
|
+
"Jul",
|
|
1129
|
+
"Aug",
|
|
1130
|
+
"Sep",
|
|
1131
|
+
"Oct",
|
|
1132
|
+
"Nov",
|
|
1133
|
+
"Dec"
|
|
1134
|
+
];
|
|
1135
|
+
switch (timeRange) {
|
|
1136
|
+
case "7d": return `${[
|
|
1137
|
+
"Sun",
|
|
1138
|
+
"Mon",
|
|
1139
|
+
"Tue",
|
|
1140
|
+
"Wed",
|
|
1141
|
+
"Thu",
|
|
1142
|
+
"Fri",
|
|
1143
|
+
"Sat"
|
|
1144
|
+
][date.getDay()]} ${date.getDate()}`;
|
|
1145
|
+
case "30d": return `${months[date.getMonth()]} ${date.getDate()}`;
|
|
1146
|
+
case "90d": return `W${Math.ceil(date.getDate() / 7)} ${months[date.getMonth()]}`;
|
|
1147
|
+
case "1y": return `${months[date.getMonth()]} ${date.getFullYear()}`;
|
|
1148
|
+
default: return `${months[date.getMonth()]} ${date.getDate()}`;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
//#endregion
|
|
1153
|
+
//#region src/client/components/widgets/progress-widget.tsx
|
|
1154
|
+
/**
|
|
1155
|
+
* Progress Widget
|
|
1156
|
+
*
|
|
1157
|
+
* Displays progress towards a goal.
|
|
1158
|
+
* Uses WidgetCard for consistent styling.
|
|
1159
|
+
*/
|
|
1160
|
+
/**
|
|
1161
|
+
* Progress Widget Component
|
|
1162
|
+
*
|
|
1163
|
+
* Displays a progress bar with current/target values.
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```tsx
|
|
1167
|
+
* <ProgressWidget
|
|
1168
|
+
* config={{
|
|
1169
|
+
* type: "progress",
|
|
1170
|
+
* id: "monthly-sales",
|
|
1171
|
+
* title: "Monthly Sales Goal",
|
|
1172
|
+
* fetchFn: async (client) => ({
|
|
1173
|
+
* current: 75000,
|
|
1174
|
+
* target: 100000,
|
|
1175
|
+
* label: "$75,000 / $100,000"
|
|
1176
|
+
* }),
|
|
1177
|
+
* showPercentage: true,
|
|
1178
|
+
* }}
|
|
1179
|
+
* />
|
|
1180
|
+
* ```
|
|
1181
|
+
*/
|
|
1182
|
+
function ProgressWidget({ config }) {
|
|
1183
|
+
const client = useAdminStore(selectClient);
|
|
1184
|
+
const resolveText = useResolveText();
|
|
1185
|
+
const { color, showPercentage = true } = config;
|
|
1186
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
1187
|
+
queryKey: [
|
|
1188
|
+
"widget",
|
|
1189
|
+
"progress",
|
|
1190
|
+
config.id
|
|
1191
|
+
],
|
|
1192
|
+
queryFn: () => config.fetchFn(client),
|
|
1193
|
+
refetchInterval: config.refreshInterval
|
|
1194
|
+
});
|
|
1195
|
+
const title = config.title ? resolveText(config.title) : void 0;
|
|
1196
|
+
const percentage = data ? Math.min(data.current / data.target * 100, 100) : 0;
|
|
1197
|
+
const percentageFormatted = percentage.toFixed(0);
|
|
1198
|
+
const getProgressColor = () => {
|
|
1199
|
+
if (color) return color;
|
|
1200
|
+
if (percentage >= 100) return "bg-green-500";
|
|
1201
|
+
if (percentage >= 75) return "bg-primary";
|
|
1202
|
+
if (percentage >= 50) return "bg-yellow-500";
|
|
1203
|
+
return "bg-muted-foreground";
|
|
1204
|
+
};
|
|
1205
|
+
const progressContent = data ? /* @__PURE__ */ jsxs("div", {
|
|
1206
|
+
className: "space-y-3",
|
|
1207
|
+
children: [
|
|
1208
|
+
/* @__PURE__ */ jsx("div", {
|
|
1209
|
+
className: "relative",
|
|
1210
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1211
|
+
className: "h-2 w-full overflow-hidden rounded-full bg-muted",
|
|
1212
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1213
|
+
className: cn("h-full rounded-full transition-all duration-500", getProgressColor()),
|
|
1214
|
+
style: { width: `${percentage}%` }
|
|
1215
|
+
})
|
|
1216
|
+
})
|
|
1217
|
+
}),
|
|
1218
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1219
|
+
className: "flex items-center justify-between text-sm",
|
|
1220
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1221
|
+
className: "text-muted-foreground",
|
|
1222
|
+
children: data.label || `${data.current.toLocaleString()} / ${data.target.toLocaleString()}`
|
|
1223
|
+
}), showPercentage && /* @__PURE__ */ jsxs("span", {
|
|
1224
|
+
className: "font-medium",
|
|
1225
|
+
children: [percentageFormatted, "%"]
|
|
1226
|
+
})]
|
|
1227
|
+
}),
|
|
1228
|
+
data.subtitle && /* @__PURE__ */ jsx("p", {
|
|
1229
|
+
className: "text-xs text-muted-foreground",
|
|
1230
|
+
children: data.subtitle
|
|
1231
|
+
})
|
|
1232
|
+
]
|
|
1233
|
+
}) : null;
|
|
1234
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1235
|
+
title,
|
|
1236
|
+
isLoading,
|
|
1237
|
+
loadingSkeleton: /* @__PURE__ */ jsx(ProgressWidgetSkeleton, {}),
|
|
1238
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
1239
|
+
onRefresh: () => refetch(),
|
|
1240
|
+
children: progressContent
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/client/components/widgets/quick-actions-widget.tsx
|
|
1246
|
+
/**
|
|
1247
|
+
* Quick Actions Widget
|
|
1248
|
+
*
|
|
1249
|
+
* Displays shortcuts to common actions with icons and proper styling.
|
|
1250
|
+
* Uses WidgetCard for consistent styling.
|
|
1251
|
+
*/
|
|
1252
|
+
/**
|
|
1253
|
+
* Quick actions widget component
|
|
1254
|
+
*
|
|
1255
|
+
* Displays a list of action items with icons, matching the style
|
|
1256
|
+
* of other dashboard widgets like recent-items.
|
|
1257
|
+
*/
|
|
1258
|
+
function QuickActionsWidget({ config, basePath = "/admin", navigate }) {
|
|
1259
|
+
const resolveText = useResolveText();
|
|
1260
|
+
const { quickActions, layout = "list" } = config;
|
|
1261
|
+
const title = config.title ? resolveText(config.title) : "Quick Actions";
|
|
1262
|
+
const parsedActions = quickActions.map((action, index) => {
|
|
1263
|
+
if (typeof action === "string") {
|
|
1264
|
+
const [collection, actionType] = action.split(".");
|
|
1265
|
+
return {
|
|
1266
|
+
id: `${action}-${index}`,
|
|
1267
|
+
label: `${actionType === "create" ? "New" : actionType} ${formatCollectionName(collection)}`,
|
|
1268
|
+
href: `${basePath}/collections/${collection}/${actionType === "create" ? "create" : ""}`,
|
|
1269
|
+
icon: void 0,
|
|
1270
|
+
variant: "default"
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
id: `action-${index}`,
|
|
1275
|
+
label: resolveText(action.label),
|
|
1276
|
+
href: action.href,
|
|
1277
|
+
onClick: action.onClick,
|
|
1278
|
+
icon: action.icon,
|
|
1279
|
+
variant: action.variant || "default"
|
|
1280
|
+
};
|
|
1281
|
+
});
|
|
1282
|
+
const handleClick = (action) => {
|
|
1283
|
+
if (action.onClick) action.onClick();
|
|
1284
|
+
else if (action.href && navigate) navigate(action.href);
|
|
1285
|
+
};
|
|
1286
|
+
const variantStyles$3 = {
|
|
1287
|
+
default: "hover:bg-muted/50 cursor-pointer",
|
|
1288
|
+
primary: "bg-primary/5 hover:bg-primary/10 border-primary/20 text-primary [&_svg]:text-primary cursor-pointer",
|
|
1289
|
+
secondary: "hover:bg-secondary cursor-pointer",
|
|
1290
|
+
outline: "border border-border hover:bg-muted/50 cursor-pointer"
|
|
1291
|
+
};
|
|
1292
|
+
const iconVariantStyles = {
|
|
1293
|
+
default: "bg-muted text-muted-foreground",
|
|
1294
|
+
primary: "bg-primary/10 text-primary",
|
|
1295
|
+
secondary: "bg-secondary text-secondary-foreground",
|
|
1296
|
+
outline: "bg-background text-muted-foreground"
|
|
1297
|
+
};
|
|
1298
|
+
if (parsedActions.length === 0) return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1299
|
+
title,
|
|
1300
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1301
|
+
className: "text-sm text-muted-foreground",
|
|
1302
|
+
children: "No actions configured"
|
|
1303
|
+
})
|
|
1304
|
+
});
|
|
1305
|
+
if (layout === "grid") return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1306
|
+
title,
|
|
1307
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1308
|
+
className: "grid grid-cols-2 gap-2",
|
|
1309
|
+
children: parsedActions.map((action) => {
|
|
1310
|
+
const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
|
|
1311
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
1312
|
+
type: "button",
|
|
1313
|
+
onClick: () => handleClick(action),
|
|
1314
|
+
className: cn("flex flex-col items-center justify-center gap-2 rounded-md p-3 text-center transition-colors", variantStyles$3[action.variant]),
|
|
1315
|
+
children: [Icon && /* @__PURE__ */ jsx("div", {
|
|
1316
|
+
className: cn("flex h-9 w-9 items-center justify-center rounded-md", iconVariantStyles[action.variant]),
|
|
1317
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
1318
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1319
|
+
className: "text-xs font-medium",
|
|
1320
|
+
children: action.label
|
|
1321
|
+
})]
|
|
1322
|
+
}, action.id);
|
|
1323
|
+
})
|
|
1324
|
+
})
|
|
1325
|
+
});
|
|
1326
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1327
|
+
title,
|
|
1328
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1329
|
+
className: "space-y-1 -mx-1",
|
|
1330
|
+
children: parsedActions.map((action) => {
|
|
1331
|
+
const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
|
|
1332
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
1333
|
+
type: "button",
|
|
1334
|
+
onClick: () => handleClick(action),
|
|
1335
|
+
className: cn("flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition-colors", variantStyles$3[action.variant]),
|
|
1336
|
+
children: [
|
|
1337
|
+
Icon && /* @__PURE__ */ jsx("div", {
|
|
1338
|
+
className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-md", iconVariantStyles[action.variant]),
|
|
1339
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
1340
|
+
}),
|
|
1341
|
+
/* @__PURE__ */ jsx("span", {
|
|
1342
|
+
className: "flex-1 text-sm font-medium truncate",
|
|
1343
|
+
children: action.label
|
|
1344
|
+
}),
|
|
1345
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" })
|
|
1346
|
+
]
|
|
1347
|
+
}, action.id);
|
|
1348
|
+
})
|
|
1349
|
+
})
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/client/components/widgets/recent-items-widget.tsx
|
|
1355
|
+
/**
|
|
1356
|
+
* Recent Items Widget
|
|
1357
|
+
*
|
|
1358
|
+
* Displays latest items from a collection.
|
|
1359
|
+
* Uses WidgetCard for consistent styling.
|
|
1360
|
+
*/
|
|
1361
|
+
/**
|
|
1362
|
+
* Recent items widget component
|
|
1363
|
+
*
|
|
1364
|
+
* Displays:
|
|
1365
|
+
* - List of most recently created/updated items
|
|
1366
|
+
* - Configurable number of items
|
|
1367
|
+
* - Links to edit each item
|
|
1368
|
+
*/
|
|
1369
|
+
function RecentItemsWidget({ config }) {
|
|
1370
|
+
const resolveText = useResolveText();
|
|
1371
|
+
const { collection, limit = 5, label, titleField = "_title", dateField = "createdAt", subtitleFields, onItemClick } = config;
|
|
1372
|
+
const { data, isLoading, error, refetch } = useCollectionList(collection, {
|
|
1373
|
+
orderBy: { [dateField]: "desc" },
|
|
1374
|
+
limit
|
|
1375
|
+
});
|
|
1376
|
+
const items = Array.isArray(data?.docs) ? data.docs : [];
|
|
1377
|
+
const displayLabel = label ? resolveText(label) : `Recent ${formatCollectionName(collection)}`;
|
|
1378
|
+
const getTitleValue = (item) => {
|
|
1379
|
+
if (titleField && item[titleField]) return String(item[titleField]);
|
|
1380
|
+
if (item._title) return String(item._title);
|
|
1381
|
+
if (item.name) return String(item.name);
|
|
1382
|
+
if (item.title) return String(item.title);
|
|
1383
|
+
if (item.label) return String(item.label);
|
|
1384
|
+
return `Item #${item.id}`;
|
|
1385
|
+
};
|
|
1386
|
+
const getSubtitle = (item) => {
|
|
1387
|
+
if (!subtitleFields?.length) return null;
|
|
1388
|
+
return subtitleFields.map((field) => item[field]).filter(Boolean).join(" - ");
|
|
1389
|
+
};
|
|
1390
|
+
const handleItemClick = (item) => {
|
|
1391
|
+
if (onItemClick) onItemClick(item);
|
|
1392
|
+
};
|
|
1393
|
+
const listContent = items.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
1394
|
+
className: "text-sm text-muted-foreground",
|
|
1395
|
+
children: "No items yet"
|
|
1396
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1397
|
+
className: "space-y-1",
|
|
1398
|
+
children: items.map((item) => {
|
|
1399
|
+
const dateValue = item[dateField];
|
|
1400
|
+
const subtitle = getSubtitle(item);
|
|
1401
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1402
|
+
type: "button",
|
|
1403
|
+
onClick: () => handleItemClick(item),
|
|
1404
|
+
className: "flex w-full items-center gap-3 rounded-md p-2 hover:bg-muted/50 cursor-pointer transition-colors text-left",
|
|
1405
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1406
|
+
className: "flex-1 min-w-0",
|
|
1407
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1408
|
+
className: "text-sm font-medium truncate",
|
|
1409
|
+
children: getTitleValue(item)
|
|
1410
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1411
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
1412
|
+
children: [dateValue && /* @__PURE__ */ jsx("span", { children: formatRelativeTime(new Date(dateValue)) }), subtitle && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "-" }), /* @__PURE__ */ jsx("span", {
|
|
1413
|
+
className: "truncate",
|
|
1414
|
+
children: subtitle
|
|
1415
|
+
})] })]
|
|
1416
|
+
})]
|
|
1417
|
+
})
|
|
1418
|
+
}, item.id);
|
|
1419
|
+
})
|
|
1420
|
+
});
|
|
1421
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1422
|
+
title: displayLabel,
|
|
1423
|
+
isLoading,
|
|
1424
|
+
loadingSkeleton: /* @__PURE__ */ jsx(RecentItemsWidgetSkeleton, { count: limit }),
|
|
1425
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
1426
|
+
onRefresh: () => refetch(),
|
|
1427
|
+
children: listContent
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Format relative time
|
|
1432
|
+
*/
|
|
1433
|
+
function formatRelativeTime(date) {
|
|
1434
|
+
const diff = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
1435
|
+
const seconds = Math.floor(diff / 1e3);
|
|
1436
|
+
const minutes = Math.floor(seconds / 60);
|
|
1437
|
+
const hours = Math.floor(minutes / 60);
|
|
1438
|
+
const days = Math.floor(hours / 24);
|
|
1439
|
+
if (days > 0) return `${days}d ago`;
|
|
1440
|
+
if (hours > 0) return `${hours}h ago`;
|
|
1441
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
1442
|
+
return "just now";
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region src/client/components/widgets/stats-widget.tsx
|
|
1447
|
+
/**
|
|
1448
|
+
* Get date range for a preset
|
|
1449
|
+
*/
|
|
1450
|
+
function getDateRange(preset) {
|
|
1451
|
+
const now = /* @__PURE__ */ new Date();
|
|
1452
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1453
|
+
switch (preset) {
|
|
1454
|
+
case "today": return {
|
|
1455
|
+
gte: today,
|
|
1456
|
+
lte: /* @__PURE__ */ new Date(today.getTime() + 1440 * 60 * 1e3 - 1)
|
|
1457
|
+
};
|
|
1458
|
+
case "yesterday": return {
|
|
1459
|
+
gte: /* @__PURE__ */ new Date(today.getTime() - 1440 * 60 * 1e3),
|
|
1460
|
+
lte: /* @__PURE__ */ new Date(today.getTime() - 1)
|
|
1461
|
+
};
|
|
1462
|
+
case "thisWeek": {
|
|
1463
|
+
const dayOfWeek = today.getDay();
|
|
1464
|
+
return {
|
|
1465
|
+
gte: /* @__PURE__ */ new Date(today.getTime() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) * 24 * 60 * 60 * 1e3),
|
|
1466
|
+
lte: now
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
case "lastWeek": {
|
|
1470
|
+
const dayOfWeek = today.getDay();
|
|
1471
|
+
const thisMonday = /* @__PURE__ */ new Date(today.getTime() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) * 24 * 60 * 60 * 1e3);
|
|
1472
|
+
return {
|
|
1473
|
+
gte: /* @__PURE__ */ new Date(thisMonday.getTime() - 10080 * 60 * 1e3),
|
|
1474
|
+
lte: /* @__PURE__ */ new Date(thisMonday.getTime() - 1)
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
case "thisMonth": return {
|
|
1478
|
+
gte: new Date(now.getFullYear(), now.getMonth(), 1),
|
|
1479
|
+
lte: now
|
|
1480
|
+
};
|
|
1481
|
+
case "lastMonth": {
|
|
1482
|
+
const firstOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1483
|
+
return {
|
|
1484
|
+
gte: new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
|
1485
|
+
lte: /* @__PURE__ */ new Date(firstOfThisMonth.getTime() - 1)
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
case "last7days": return {
|
|
1489
|
+
gte: /* @__PURE__ */ new Date(now.getTime() - 10080 * 60 * 1e3),
|
|
1490
|
+
lte: now
|
|
1491
|
+
};
|
|
1492
|
+
case "last30days": return {
|
|
1493
|
+
gte: /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3),
|
|
1494
|
+
lte: now
|
|
1495
|
+
};
|
|
1496
|
+
case "last90days": return {
|
|
1497
|
+
gte: /* @__PURE__ */ new Date(now.getTime() - 2160 * 60 * 60 * 1e3),
|
|
1498
|
+
lte: now
|
|
1499
|
+
};
|
|
1500
|
+
case "thisYear": return {
|
|
1501
|
+
gte: new Date(now.getFullYear(), 0, 1),
|
|
1502
|
+
lte: now
|
|
1503
|
+
};
|
|
1504
|
+
case "lastYear": return {
|
|
1505
|
+
gte: new Date(now.getFullYear() - 1, 0, 1),
|
|
1506
|
+
lte: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59)
|
|
1507
|
+
};
|
|
1508
|
+
default: return {
|
|
1509
|
+
gte: today,
|
|
1510
|
+
lte: now
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const variantStyles$1 = {
|
|
1515
|
+
default: "",
|
|
1516
|
+
primary: "border-primary/30 bg-primary/5",
|
|
1517
|
+
success: "border-green-500/30 bg-green-500/5",
|
|
1518
|
+
warning: "border-yellow-500/30 bg-yellow-500/5",
|
|
1519
|
+
danger: "border-red-500/30 bg-red-500/5"
|
|
1520
|
+
};
|
|
1521
|
+
const variantValueStyles = {
|
|
1522
|
+
default: "",
|
|
1523
|
+
primary: "text-primary",
|
|
1524
|
+
success: "text-green-600 dark:text-green-400",
|
|
1525
|
+
warning: "text-yellow-600 dark:text-yellow-400",
|
|
1526
|
+
danger: "text-red-600 dark:text-red-400"
|
|
1527
|
+
};
|
|
1528
|
+
/**
|
|
1529
|
+
* Stats widget component
|
|
1530
|
+
*
|
|
1531
|
+
* Shows:
|
|
1532
|
+
* - Total count for a collection
|
|
1533
|
+
* - Optional icon
|
|
1534
|
+
* - Color variant for visual distinction
|
|
1535
|
+
*/
|
|
1536
|
+
function StatsWidget({ config }) {
|
|
1537
|
+
const resolveText = useResolveText();
|
|
1538
|
+
const { collection, label, filter, filterFn, dateFilter, icon: Icon, variant = "default" } = config;
|
|
1539
|
+
const computedFilter = useMemo(() => {
|
|
1540
|
+
let result = {};
|
|
1541
|
+
if (filter) result = {
|
|
1542
|
+
...result,
|
|
1543
|
+
...filter
|
|
1544
|
+
};
|
|
1545
|
+
if (filterFn) result = {
|
|
1546
|
+
...result,
|
|
1547
|
+
...filterFn()
|
|
1548
|
+
};
|
|
1549
|
+
if (dateFilter) {
|
|
1550
|
+
const { gte, lte } = getDateRange(dateFilter.range);
|
|
1551
|
+
result[dateFilter.field] = {
|
|
1552
|
+
gte: gte.toISOString(),
|
|
1553
|
+
lte: lte.toISOString()
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1557
|
+
}, [
|
|
1558
|
+
filter,
|
|
1559
|
+
filterFn,
|
|
1560
|
+
dateFilter
|
|
1561
|
+
]);
|
|
1562
|
+
const { data: count = 0, isLoading, error, refetch } = useCollectionCount(collection, computedFilter ? { where: computedFilter } : void 0);
|
|
1563
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1564
|
+
title: label ? resolveText(label) : formatCollectionName(collection),
|
|
1565
|
+
icon: Icon && typeof Icon !== "string" ? Icon : void 0,
|
|
1566
|
+
isLoading,
|
|
1567
|
+
loadingSkeleton: /* @__PURE__ */ jsx(StatsWidgetSkeleton, {}),
|
|
1568
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
1569
|
+
onRefresh: () => refetch(),
|
|
1570
|
+
className: variantStyles$1[variant],
|
|
1571
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1572
|
+
className: cn("text-2xl font-bold", variantValueStyles[variant]),
|
|
1573
|
+
children: count.toLocaleString()
|
|
1574
|
+
})
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region src/client/components/widgets/table-widget.tsx
|
|
1580
|
+
/**
|
|
1581
|
+
* Field types that need fieldDef passed to their cell component
|
|
1582
|
+
*/
|
|
1583
|
+
const FIELD_TYPES_NEEDING_FIELD_DEF = new Set([
|
|
1584
|
+
"object",
|
|
1585
|
+
"array",
|
|
1586
|
+
"relation",
|
|
1587
|
+
"reverseRelation"
|
|
1588
|
+
]);
|
|
1589
|
+
/**
|
|
1590
|
+
* Normalize column config - converts string to object format
|
|
1591
|
+
*/
|
|
1592
|
+
function normalizeColumn(column) {
|
|
1593
|
+
if (typeof column === "string") return { key: column };
|
|
1594
|
+
return column;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Get column label from field definition or column config
|
|
1598
|
+
*/
|
|
1599
|
+
function getColumnLabel(column, fieldDef) {
|
|
1600
|
+
if (column.label) return column.label;
|
|
1601
|
+
const fieldOptions = fieldDef?.["~options"];
|
|
1602
|
+
if (fieldOptions?.label) return fieldOptions.label;
|
|
1603
|
+
return column.key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Resolve cell component from field definition
|
|
1607
|
+
*/
|
|
1608
|
+
function resolveCellComponent(fieldDef) {
|
|
1609
|
+
if (fieldDef?.cell?.component) return fieldDef.cell.component;
|
|
1610
|
+
return DefaultCell;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Table Widget Component
|
|
1614
|
+
*
|
|
1615
|
+
* Displays a mini table of collection items.
|
|
1616
|
+
* Uses field definitions from admin config for labels and cell rendering.
|
|
1617
|
+
*/
|
|
1618
|
+
function TableWidget({ config, basePath = "/admin", navigate }) {
|
|
1619
|
+
const resolveText = useResolveText();
|
|
1620
|
+
const admin = useAdminStore(selectAdmin);
|
|
1621
|
+
const { collection, columns: rawColumns, limit = 5, sortBy, sortOrder = "desc", filter, linkToDetail, emptyMessage } = config;
|
|
1622
|
+
const columns = rawColumns.map(normalizeColumn);
|
|
1623
|
+
const fields = (admin?.getCollections() ?? {})[collection]?.fields;
|
|
1624
|
+
const queryOptions = { limit };
|
|
1625
|
+
if (sortBy) queryOptions.orderBy = { [sortBy]: sortOrder };
|
|
1626
|
+
if (filter) queryOptions.where = filter;
|
|
1627
|
+
const { data, isLoading, error, refetch } = useCollectionList(collection, queryOptions);
|
|
1628
|
+
const items = Array.isArray(data?.docs) ? data.docs : [];
|
|
1629
|
+
const title = config.title ? resolveText(config.title) : void 0;
|
|
1630
|
+
const handleRowClick = (item) => {
|
|
1631
|
+
if (linkToDetail && navigate) navigate(`${basePath}/collections/${collection}/${item.id}`);
|
|
1632
|
+
};
|
|
1633
|
+
const renderCell = (item, column) => {
|
|
1634
|
+
const value = item[column.key];
|
|
1635
|
+
if (column.render) return column.render(value, item);
|
|
1636
|
+
const fieldDef = fields?.[column.key];
|
|
1637
|
+
const CellComponent = resolveCellComponent(fieldDef);
|
|
1638
|
+
const fieldType = fieldDef?.name ?? "text";
|
|
1639
|
+
return FIELD_TYPES_NEEDING_FIELD_DEF.has(fieldType) ? /* @__PURE__ */ jsx(CellComponent, {
|
|
1640
|
+
value,
|
|
1641
|
+
row: item,
|
|
1642
|
+
fieldDef
|
|
1643
|
+
}) : /* @__PURE__ */ jsx(CellComponent, {
|
|
1644
|
+
value,
|
|
1645
|
+
row: item
|
|
1646
|
+
});
|
|
1647
|
+
};
|
|
1648
|
+
const emptyContent = /* @__PURE__ */ jsx("div", {
|
|
1649
|
+
className: "flex h-24 items-center justify-center text-muted-foreground",
|
|
1650
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1651
|
+
className: "text-sm",
|
|
1652
|
+
children: emptyMessage ? resolveText(emptyMessage) : "No data available"
|
|
1653
|
+
})
|
|
1654
|
+
});
|
|
1655
|
+
const tableContent = items.length === 0 ? emptyContent : /* @__PURE__ */ jsxs("div", {
|
|
1656
|
+
className: "-mx-5 -mb-1",
|
|
1657
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1658
|
+
className: "flex items-center gap-2 px-5 py-2 border-b border-border/30 text-[10px] font-medium uppercase tracking-wider text-muted-foreground bg-muted/20 backdrop-blur-sm",
|
|
1659
|
+
children: columns.map((column) => {
|
|
1660
|
+
const fieldDef = fields?.[column.key];
|
|
1661
|
+
const label = getColumnLabel(column, fieldDef);
|
|
1662
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1663
|
+
className: cn("flex-1 min-w-0", column.align === "center" && "text-center", column.align === "right" && "text-right"),
|
|
1664
|
+
style: column.width ? {
|
|
1665
|
+
width: column.width,
|
|
1666
|
+
flex: "none"
|
|
1667
|
+
} : void 0,
|
|
1668
|
+
children: resolveText(label)
|
|
1669
|
+
}, column.key);
|
|
1670
|
+
})
|
|
1671
|
+
}), items.map((item) => linkToDetail ? /* @__PURE__ */ jsx("button", {
|
|
1672
|
+
type: "button",
|
|
1673
|
+
className: "flex w-full items-center gap-2 px-5 py-2.5 border-b border-border/20 last:border-0 transition-all cursor-pointer hover:bg-muted/30 hover:backdrop-blur-sm text-left",
|
|
1674
|
+
onClick: () => handleRowClick(item),
|
|
1675
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("div", {
|
|
1676
|
+
className: cn("flex-1 min-w-0 text-sm truncate", column.align === "center" && "text-center", column.align === "right" && "text-right"),
|
|
1677
|
+
style: column.width ? {
|
|
1678
|
+
width: column.width,
|
|
1679
|
+
flex: "none"
|
|
1680
|
+
} : void 0,
|
|
1681
|
+
children: renderCell(item, column)
|
|
1682
|
+
}, column.key))
|
|
1683
|
+
}, item.id) : /* @__PURE__ */ jsx("div", {
|
|
1684
|
+
className: "flex items-center gap-2 px-5 py-2.5 border-b border-border/20 last:border-0",
|
|
1685
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("div", {
|
|
1686
|
+
className: cn("flex-1 min-w-0 text-sm truncate", column.align === "center" && "text-center", column.align === "right" && "text-right"),
|
|
1687
|
+
style: column.width ? {
|
|
1688
|
+
width: column.width,
|
|
1689
|
+
flex: "none"
|
|
1690
|
+
} : void 0,
|
|
1691
|
+
children: renderCell(item, column)
|
|
1692
|
+
}, column.key))
|
|
1693
|
+
}, item.id))]
|
|
1694
|
+
});
|
|
1695
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1696
|
+
title,
|
|
1697
|
+
isLoading,
|
|
1698
|
+
loadingSkeleton: /* @__PURE__ */ jsx(TableWidgetSkeleton, {
|
|
1699
|
+
rows: limit,
|
|
1700
|
+
columns: columns.length
|
|
1701
|
+
}),
|
|
1702
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
1703
|
+
onRefresh: () => refetch(),
|
|
1704
|
+
children: tableContent
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
//#endregion
|
|
1709
|
+
//#region src/client/components/widgets/timeline-widget.tsx
|
|
1710
|
+
/**
|
|
1711
|
+
* Timeline Widget
|
|
1712
|
+
*
|
|
1713
|
+
* Displays an activity/event timeline.
|
|
1714
|
+
* Uses WidgetCard for consistent styling.
|
|
1715
|
+
*/
|
|
1716
|
+
const variantStyles = {
|
|
1717
|
+
default: "bg-muted-foreground",
|
|
1718
|
+
success: "bg-green-500",
|
|
1719
|
+
warning: "bg-yellow-500",
|
|
1720
|
+
error: "bg-red-500",
|
|
1721
|
+
info: "bg-blue-500"
|
|
1722
|
+
};
|
|
1723
|
+
/**
|
|
1724
|
+
* Format timestamp based on format option
|
|
1725
|
+
*/
|
|
1726
|
+
function formatTimestamp(date, format = "relative") {
|
|
1727
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
1728
|
+
switch (format) {
|
|
1729
|
+
case "absolute": return d.toLocaleDateString();
|
|
1730
|
+
case "datetime": return d.toLocaleString();
|
|
1731
|
+
case "relative":
|
|
1732
|
+
default: {
|
|
1733
|
+
const diff = (/* @__PURE__ */ new Date()).getTime() - d.getTime();
|
|
1734
|
+
const seconds = Math.floor(diff / 1e3);
|
|
1735
|
+
const minutes = Math.floor(seconds / 60);
|
|
1736
|
+
const hours = Math.floor(minutes / 60);
|
|
1737
|
+
const days = Math.floor(hours / 24);
|
|
1738
|
+
if (days > 7) return d.toLocaleDateString();
|
|
1739
|
+
if (days > 0) return `${days}d ago`;
|
|
1740
|
+
if (hours > 0) return `${hours}h ago`;
|
|
1741
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
1742
|
+
return "just now";
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Timeline Widget Component
|
|
1748
|
+
*
|
|
1749
|
+
* Displays a vertical timeline of events/activities.
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```tsx
|
|
1753
|
+
* <TimelineWidget
|
|
1754
|
+
* config={{
|
|
1755
|
+
* type: "timeline",
|
|
1756
|
+
* id: "recent-activity",
|
|
1757
|
+
* title: "Recent Activity",
|
|
1758
|
+
* fetchFn: async (client) => {
|
|
1759
|
+
* const activities = await client.collections.activities.findMany({
|
|
1760
|
+
* limit: 10,
|
|
1761
|
+
* orderBy: { createdAt: "desc" }
|
|
1762
|
+
* });
|
|
1763
|
+
* return activities.map(a => ({
|
|
1764
|
+
* id: a.id,
|
|
1765
|
+
* title: a.action,
|
|
1766
|
+
* description: a.description,
|
|
1767
|
+
* timestamp: a.createdAt,
|
|
1768
|
+
* variant: a.type === "error" ? "error" : "default"
|
|
1769
|
+
* }));
|
|
1770
|
+
* }
|
|
1771
|
+
* }}
|
|
1772
|
+
* />
|
|
1773
|
+
* ```
|
|
1774
|
+
*/
|
|
1775
|
+
function TimelineWidget({ config, navigate }) {
|
|
1776
|
+
const client = useAdminStore(selectClient);
|
|
1777
|
+
const resolveText = useResolveText();
|
|
1778
|
+
const { maxItems = 10, showTimestamps = true, timestampFormat = "relative", emptyMessage } = config;
|
|
1779
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
1780
|
+
queryKey: [
|
|
1781
|
+
"widget",
|
|
1782
|
+
"timeline",
|
|
1783
|
+
config.id
|
|
1784
|
+
],
|
|
1785
|
+
queryFn: () => config.fetchFn(client),
|
|
1786
|
+
refetchInterval: config.refreshInterval
|
|
1787
|
+
});
|
|
1788
|
+
const items = data?.slice(0, maxItems) ?? [];
|
|
1789
|
+
const title = config.title ? resolveText(config.title) : void 0;
|
|
1790
|
+
const handleItemClick = (item) => {
|
|
1791
|
+
if (item.href && navigate) navigate(item.href);
|
|
1792
|
+
};
|
|
1793
|
+
const emptyContent = /* @__PURE__ */ jsx("div", {
|
|
1794
|
+
className: "flex h-24 items-center justify-center text-muted-foreground",
|
|
1795
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1796
|
+
className: "text-sm",
|
|
1797
|
+
children: emptyMessage ? resolveText(emptyMessage) : "No activity yet"
|
|
1798
|
+
})
|
|
1799
|
+
});
|
|
1800
|
+
const timelineContent = items.length === 0 ? emptyContent : /* @__PURE__ */ jsx("div", {
|
|
1801
|
+
className: "space-y-0",
|
|
1802
|
+
children: items.map((item, index) => {
|
|
1803
|
+
const Icon = item.icon || Circle;
|
|
1804
|
+
const variant = item.variant || "default";
|
|
1805
|
+
const isLast = index === items.length - 1;
|
|
1806
|
+
const isClickable = !!(item.href && navigate);
|
|
1807
|
+
const itemContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1808
|
+
!isLast && /* @__PURE__ */ jsx("div", { className: "absolute left-[11px] top-6 bottom-0 w-px bg-border" }),
|
|
1809
|
+
/* @__PURE__ */ jsx("div", {
|
|
1810
|
+
className: cn("relative z-10 flex h-6 w-6 shrink-0 items-center justify-center rounded-full", variantStyles[variant]),
|
|
1811
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1812
|
+
className: "h-3 w-3 text-white",
|
|
1813
|
+
weight: "bold"
|
|
1814
|
+
})
|
|
1815
|
+
}),
|
|
1816
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1817
|
+
className: "flex-1 min-w-0 pt-0.5 text-left",
|
|
1818
|
+
children: [
|
|
1819
|
+
/* @__PURE__ */ jsx("p", {
|
|
1820
|
+
className: "text-sm font-medium truncate",
|
|
1821
|
+
children: item.title
|
|
1822
|
+
}),
|
|
1823
|
+
item.description && /* @__PURE__ */ jsx("p", {
|
|
1824
|
+
className: "text-xs text-muted-foreground mt-0.5 line-clamp-2",
|
|
1825
|
+
children: item.description
|
|
1826
|
+
}),
|
|
1827
|
+
showTimestamps && item.timestamp && /* @__PURE__ */ jsx("p", {
|
|
1828
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
1829
|
+
children: formatTimestamp(item.timestamp, timestampFormat)
|
|
1830
|
+
})
|
|
1831
|
+
]
|
|
1832
|
+
})
|
|
1833
|
+
] });
|
|
1834
|
+
if (isClickable) return /* @__PURE__ */ jsx("button", {
|
|
1835
|
+
type: "button",
|
|
1836
|
+
className: cn("relative flex gap-3 pb-4 w-full", "cursor-pointer hover:bg-muted/30 -mx-2 px-2 rounded-md transition-colors", isLast && "pb-0"),
|
|
1837
|
+
onClick: () => handleItemClick(item),
|
|
1838
|
+
children: itemContent
|
|
1839
|
+
}, item.id);
|
|
1840
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1841
|
+
className: cn("relative flex gap-3 pb-4", isLast && "pb-0"),
|
|
1842
|
+
children: itemContent
|
|
1843
|
+
}, item.id);
|
|
1844
|
+
})
|
|
1845
|
+
});
|
|
1846
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1847
|
+
title,
|
|
1848
|
+
isLoading,
|
|
1849
|
+
loadingSkeleton: /* @__PURE__ */ jsx(TimelineWidgetSkeleton, { count: maxItems }),
|
|
1850
|
+
error: error instanceof Error ? error : error ? new Error(String(error)) : null,
|
|
1851
|
+
onRefresh: () => refetch(),
|
|
1852
|
+
children: timelineContent
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/client/components/widgets/value-widget.tsx
|
|
1858
|
+
/**
|
|
1859
|
+
* Value Widget
|
|
1860
|
+
*
|
|
1861
|
+
* Flexible widget with async data fetching and full Tailwind customization.
|
|
1862
|
+
* Uses WidgetCard for consistent styling while allowing custom classNames.
|
|
1863
|
+
*/
|
|
1864
|
+
/**
|
|
1865
|
+
* Format value for display
|
|
1866
|
+
*/
|
|
1867
|
+
function formatValue(value) {
|
|
1868
|
+
if (typeof value === "number") return value.toLocaleString();
|
|
1869
|
+
return value;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Value Widget Component
|
|
1873
|
+
*
|
|
1874
|
+
* Fetches data via async function and renders with full customization.
|
|
1875
|
+
*
|
|
1876
|
+
* @example
|
|
1877
|
+
* ```tsx
|
|
1878
|
+
* <ValueWidget
|
|
1879
|
+
* config={{
|
|
1880
|
+
* type: "value",
|
|
1881
|
+
* id: "total-revenue",
|
|
1882
|
+
* fetchFn: async (client) => ({
|
|
1883
|
+
* value: 42,
|
|
1884
|
+
* label: "Total",
|
|
1885
|
+
* classNames: { root: "bg-blue-50" },
|
|
1886
|
+
* }),
|
|
1887
|
+
* }}
|
|
1888
|
+
* />
|
|
1889
|
+
* ```
|
|
1890
|
+
*/
|
|
1891
|
+
function ValueWidget({ config }) {
|
|
1892
|
+
const client = useAdminStore(selectClient);
|
|
1893
|
+
const resolveText = useResolveText();
|
|
1894
|
+
const { data, isLoading, error, refetch, isFetching } = useQuery({
|
|
1895
|
+
queryKey: [
|
|
1896
|
+
"widget",
|
|
1897
|
+
"value",
|
|
1898
|
+
config.id
|
|
1899
|
+
],
|
|
1900
|
+
queryFn: () => config.fetchFn(client),
|
|
1901
|
+
refetchInterval: config.refreshInterval
|
|
1902
|
+
});
|
|
1903
|
+
const isFeatured = config.cardVariant === "featured";
|
|
1904
|
+
if (isLoading || error || !data) return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1905
|
+
title: config.title ? resolveText(config.title) : void 0,
|
|
1906
|
+
isLoading,
|
|
1907
|
+
loadingSkeleton: /* @__PURE__ */ jsx(ValueWidgetSkeleton, { featured: isFeatured }),
|
|
1908
|
+
error: error instanceof Error ? error : !data && !isLoading ? /* @__PURE__ */ new Error("No data returned") : null,
|
|
1909
|
+
onRefresh: () => refetch(),
|
|
1910
|
+
className: data?.classNames?.root
|
|
1911
|
+
});
|
|
1912
|
+
const cls = data.classNames ?? {};
|
|
1913
|
+
const Icon = data.icon;
|
|
1914
|
+
const TrendIcon = data.trend?.icon;
|
|
1915
|
+
const label = data.label ? resolveText(data.label) : void 0;
|
|
1916
|
+
const subtitle = data.subtitle ? resolveText(data.subtitle) : void 0;
|
|
1917
|
+
const footer = data.footer ? resolveText(data.footer) : void 0;
|
|
1918
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1919
|
+
title: label,
|
|
1920
|
+
icon: Icon,
|
|
1921
|
+
onRefresh: () => refetch(),
|
|
1922
|
+
isRefreshing: isFetching && !isLoading,
|
|
1923
|
+
className: cls.root,
|
|
1924
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1925
|
+
className: cn("space-y-1", cls.content),
|
|
1926
|
+
children: [
|
|
1927
|
+
/* @__PURE__ */ jsx("div", {
|
|
1928
|
+
className: cn("text-2xl font-bold", cls.value),
|
|
1929
|
+
children: data.formatted ?? formatValue(data.value)
|
|
1930
|
+
}),
|
|
1931
|
+
data.trend && /* @__PURE__ */ jsxs("div", {
|
|
1932
|
+
className: cn("flex items-center gap-1 text-sm", cls.trend),
|
|
1933
|
+
children: [TrendIcon && /* @__PURE__ */ jsx(TrendIcon, { className: cn("h-3 w-3", cls.trendIcon) }), /* @__PURE__ */ jsx("span", { children: data.trend.value })]
|
|
1934
|
+
}),
|
|
1935
|
+
subtitle && /* @__PURE__ */ jsx("p", {
|
|
1936
|
+
className: cn("text-xs text-muted-foreground", cls.subtitle),
|
|
1937
|
+
children: subtitle
|
|
1938
|
+
}),
|
|
1939
|
+
footer && /* @__PURE__ */ jsx("p", {
|
|
1940
|
+
className: cn("text-xs text-muted-foreground pt-2", cls.footer),
|
|
1941
|
+
children: footer
|
|
1942
|
+
})
|
|
1943
|
+
]
|
|
1944
|
+
})
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
//#endregion
|
|
1949
|
+
//#region src/client/views/dashboard/dashboard-widget.tsx
|
|
1950
|
+
/**
|
|
1951
|
+
* DashboardWidget Component
|
|
1952
|
+
*
|
|
1953
|
+
* Renders individual dashboard widgets based on their type configuration.
|
|
1954
|
+
* Supports built-in widget types and custom components.
|
|
1955
|
+
*/
|
|
1956
|
+
function UnknownWidget({ type }) {
|
|
1957
|
+
return /* @__PURE__ */ jsx(WidgetCard, {
|
|
1958
|
+
title: "Unknown Widget",
|
|
1959
|
+
className: "border-yellow-500/20 bg-yellow-500/5",
|
|
1960
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1961
|
+
className: "text-xs text-muted-foreground",
|
|
1962
|
+
children: [
|
|
1963
|
+
"Widget type \"",
|
|
1964
|
+
type,
|
|
1965
|
+
"\" is not recognized."
|
|
1966
|
+
]
|
|
1967
|
+
})
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
function CustomWidgetRenderer({ loader, widgetConfig, span }) {
|
|
1971
|
+
const [state, setState] = React$1.useState({
|
|
1972
|
+
Component: null,
|
|
1973
|
+
loading: true,
|
|
1974
|
+
error: null
|
|
1975
|
+
});
|
|
1976
|
+
React$1.useEffect(() => {
|
|
1977
|
+
if (!loader) {
|
|
1978
|
+
setState({
|
|
1979
|
+
Component: null,
|
|
1980
|
+
loading: false,
|
|
1981
|
+
error: null
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (!(typeof loader === "function" && !loader.prototype?.render && !loader.prototype?.isReactComponent && loader.length === 0)) {
|
|
1986
|
+
setState({
|
|
1987
|
+
Component: loader,
|
|
1988
|
+
loading: false,
|
|
1989
|
+
error: null
|
|
1990
|
+
});
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
let mounted = true;
|
|
1994
|
+
(async () => {
|
|
1995
|
+
try {
|
|
1996
|
+
const result = await loader();
|
|
1997
|
+
if (mounted) setState({
|
|
1998
|
+
Component: result.default || result,
|
|
1999
|
+
loading: false,
|
|
2000
|
+
error: null
|
|
2001
|
+
});
|
|
2002
|
+
} catch (err) {
|
|
2003
|
+
if (mounted) setState({
|
|
2004
|
+
Component: null,
|
|
2005
|
+
loading: false,
|
|
2006
|
+
error: err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to load component")
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
})();
|
|
2010
|
+
return () => {
|
|
2011
|
+
mounted = false;
|
|
2012
|
+
};
|
|
2013
|
+
}, [loader]);
|
|
2014
|
+
if (state.loading) return /* @__PURE__ */ jsx(WidgetCard, { isLoading: true });
|
|
2015
|
+
if (state.error) return /* @__PURE__ */ jsx(WidgetCard, { error: state.error });
|
|
2016
|
+
if (!state.Component) return /* @__PURE__ */ jsx(WidgetCard, { error: /* @__PURE__ */ new Error("Component not found") });
|
|
2017
|
+
const Component = state.Component;
|
|
2018
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
2019
|
+
config: widgetConfig,
|
|
2020
|
+
span
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
function StatsWidgetRenderer({ config }) {
|
|
2024
|
+
const resolveText = useResolveText();
|
|
2025
|
+
return /* @__PURE__ */ jsx(StatsWidget, { config: {
|
|
2026
|
+
collection: config.collection,
|
|
2027
|
+
label: config.label ? resolveText(config.label) : void 0,
|
|
2028
|
+
filter: config.filter,
|
|
2029
|
+
filterFn: config.filterFn,
|
|
2030
|
+
dateFilter: config.dateFilter,
|
|
2031
|
+
icon: config.icon,
|
|
2032
|
+
variant: config.variant
|
|
2033
|
+
} });
|
|
2034
|
+
}
|
|
2035
|
+
function ChartWidgetRenderer({ config }) {
|
|
2036
|
+
const resolveText = useResolveText();
|
|
2037
|
+
return /* @__PURE__ */ jsx(ChartWidget, { config: {
|
|
2038
|
+
collection: config.collection,
|
|
2039
|
+
field: config.field,
|
|
2040
|
+
chartType: config.chartType,
|
|
2041
|
+
timeRange: config.timeRange,
|
|
2042
|
+
label: config.label ? resolveText(config.label) : void 0,
|
|
2043
|
+
color: config.color,
|
|
2044
|
+
showGrid: config.showGrid,
|
|
2045
|
+
aggregation: config.aggregation,
|
|
2046
|
+
valueField: config.valueField
|
|
2047
|
+
} });
|
|
2048
|
+
}
|
|
2049
|
+
function RecentItemsWidgetRenderer({ config, basePath, navigate }) {
|
|
2050
|
+
const resolveText = useResolveText();
|
|
2051
|
+
return /* @__PURE__ */ jsx(RecentItemsWidget, { config: {
|
|
2052
|
+
collection: config.collection,
|
|
2053
|
+
limit: config.limit,
|
|
2054
|
+
label: config.label ? resolveText(config.label) : void 0,
|
|
2055
|
+
titleField: config.titleField,
|
|
2056
|
+
dateField: config.dateField,
|
|
2057
|
+
subtitleFields: config.subtitleFields,
|
|
2058
|
+
basePath,
|
|
2059
|
+
onItemClick: navigate ? (item) => navigate(`${basePath}/collections/${config.collection}/${item.id}`) : void 0
|
|
2060
|
+
} });
|
|
2061
|
+
}
|
|
2062
|
+
function QuickActionsWidgetRenderer({ config, basePath, navigate }) {
|
|
2063
|
+
return /* @__PURE__ */ jsx(QuickActionsWidget, {
|
|
2064
|
+
config,
|
|
2065
|
+
basePath,
|
|
2066
|
+
navigate
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
function TableWidgetRenderer({ config, basePath, navigate }) {
|
|
2070
|
+
return /* @__PURE__ */ jsx(TableWidget, {
|
|
2071
|
+
config,
|
|
2072
|
+
basePath,
|
|
2073
|
+
navigate
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
function TimelineWidgetRenderer({ config, navigate }) {
|
|
2077
|
+
return /* @__PURE__ */ jsx(TimelineWidget, {
|
|
2078
|
+
config,
|
|
2079
|
+
navigate
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
function ProgressWidgetRenderer({ config }) {
|
|
2083
|
+
return /* @__PURE__ */ jsx(ProgressWidget, { config });
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* DashboardWidget - Renders a single widget based on its configuration
|
|
2087
|
+
*
|
|
2088
|
+
* @example
|
|
2089
|
+
* ```tsx
|
|
2090
|
+
* <DashboardWidget
|
|
2091
|
+
* config={{ type: "stats", collection: "posts", label: "Total Posts" }}
|
|
2092
|
+
* basePath="/admin"
|
|
2093
|
+
* navigate={navigate}
|
|
2094
|
+
* />
|
|
2095
|
+
* ```
|
|
2096
|
+
*/
|
|
2097
|
+
function DashboardWidget({ config, basePath = "/admin", navigate, widgetRegistry }) {
|
|
2098
|
+
const renderWidget = () => {
|
|
2099
|
+
if (config.type === "custom") return /* @__PURE__ */ jsx(CustomWidgetRenderer, {
|
|
2100
|
+
loader: config.component,
|
|
2101
|
+
widgetConfig: config.config || {},
|
|
2102
|
+
span: config.span
|
|
2103
|
+
});
|
|
2104
|
+
if (widgetRegistry?.[config.type]) {
|
|
2105
|
+
const CustomWidget = widgetRegistry[config.type];
|
|
2106
|
+
return /* @__PURE__ */ jsx(CustomWidget, {
|
|
2107
|
+
config,
|
|
2108
|
+
span: config.span
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
switch (config.type) {
|
|
2112
|
+
case "stats": return /* @__PURE__ */ jsx(StatsWidgetRenderer, { config });
|
|
2113
|
+
case "chart": return /* @__PURE__ */ jsx(ChartWidgetRenderer, { config });
|
|
2114
|
+
case "recentItems": return /* @__PURE__ */ jsx(RecentItemsWidgetRenderer, {
|
|
2115
|
+
config,
|
|
2116
|
+
basePath,
|
|
2117
|
+
navigate
|
|
2118
|
+
});
|
|
2119
|
+
case "quickActions": return /* @__PURE__ */ jsx(QuickActionsWidgetRenderer, {
|
|
2120
|
+
config,
|
|
2121
|
+
basePath,
|
|
2122
|
+
navigate
|
|
2123
|
+
});
|
|
2124
|
+
case "value": return /* @__PURE__ */ jsx(ValueWidget, { config });
|
|
2125
|
+
case "table": return /* @__PURE__ */ jsx(TableWidgetRenderer, {
|
|
2126
|
+
config,
|
|
2127
|
+
basePath,
|
|
2128
|
+
navigate
|
|
2129
|
+
});
|
|
2130
|
+
case "timeline": return /* @__PURE__ */ jsx(TimelineWidgetRenderer, {
|
|
2131
|
+
config,
|
|
2132
|
+
navigate
|
|
2133
|
+
});
|
|
2134
|
+
case "progress": return /* @__PURE__ */ jsx(ProgressWidgetRenderer, { config });
|
|
2135
|
+
default: return /* @__PURE__ */ jsx(UnknownWidget, { type: config.type });
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
return /* @__PURE__ */ jsx(WidgetErrorBoundary, {
|
|
2139
|
+
widgetType: config.type,
|
|
2140
|
+
children: renderWidget()
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region src/client/views/dashboard/dashboard-grid.tsx
|
|
2146
|
+
/**
|
|
2147
|
+
* Grid column classes for different column counts
|
|
2148
|
+
* Uses container queries (@container) for responsive behavior
|
|
2149
|
+
*/
|
|
2150
|
+
const gridClasses = {
|
|
2151
|
+
1: "grid-cols-1",
|
|
2152
|
+
2: "grid-cols-1 @xs:grid-cols-2",
|
|
2153
|
+
3: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3",
|
|
2154
|
+
4: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3 @lg:grid-cols-4",
|
|
2155
|
+
5: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 @lg:grid-cols-5",
|
|
2156
|
+
6: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 @lg:grid-cols-5 @xl:grid-cols-6",
|
|
2157
|
+
12: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-4 @md:grid-cols-6 @lg:grid-cols-12"
|
|
2158
|
+
};
|
|
2159
|
+
/**
|
|
2160
|
+
* Span classes for widget column spanning
|
|
2161
|
+
*/
|
|
2162
|
+
const spanClasses = {
|
|
2163
|
+
1: "col-span-1",
|
|
2164
|
+
2: "col-span-1 @xs:col-span-2",
|
|
2165
|
+
3: "col-span-1 @xs:col-span-2 @md:col-span-3",
|
|
2166
|
+
4: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-4",
|
|
2167
|
+
5: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-5",
|
|
2168
|
+
6: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-6",
|
|
2169
|
+
12: "col-span-full"
|
|
2170
|
+
};
|
|
2171
|
+
/**
|
|
2172
|
+
* Get grid columns class based on column count
|
|
2173
|
+
*/
|
|
2174
|
+
function getGridClass(columns) {
|
|
2175
|
+
return gridClasses[columns] || gridClasses[4];
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Get column span class for a widget
|
|
2179
|
+
*/
|
|
2180
|
+
function getSpanClass(span) {
|
|
2181
|
+
if (!span || span <= 1) return "";
|
|
2182
|
+
return spanClasses[span] || "";
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Check if item is a widget config
|
|
2186
|
+
*/
|
|
2187
|
+
function isWidgetConfig(item) {
|
|
2188
|
+
return typeof item === "object" && "type" in item && item.type !== "section" && item.type !== "tabs";
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Check if item is a section config
|
|
2192
|
+
*/
|
|
2193
|
+
function isSectionConfig(item) {
|
|
2194
|
+
return typeof item === "object" && "type" in item && item.type === "section";
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Check if item is a tabs config
|
|
2198
|
+
*/
|
|
2199
|
+
function isTabsConfig(item) {
|
|
2200
|
+
return typeof item === "object" && "type" in item && item.type === "tabs";
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Generate a unique key for a layout item
|
|
2204
|
+
*/
|
|
2205
|
+
function getLayoutItemKey(item, index) {
|
|
2206
|
+
if (isWidgetConfig(item)) return `widget-${item.id || item.type}-${index}`;
|
|
2207
|
+
if (isSectionConfig(item)) return `section-${index}`;
|
|
2208
|
+
if (isTabsConfig(item)) return `tabs-${item.tabs.map((t) => t.id).join("-") || index}`;
|
|
2209
|
+
return `item-${index}`;
|
|
2210
|
+
}
|
|
2211
|
+
function DashboardHeader({ title, description, actions, navigate, resolveText }) {
|
|
2212
|
+
if (!title && !description && !actions?.length) return null;
|
|
2213
|
+
const handleActionClick = (action) => {
|
|
2214
|
+
if (action.onClick) action.onClick();
|
|
2215
|
+
else if (action.href && navigate) navigate(action.href);
|
|
2216
|
+
};
|
|
2217
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2218
|
+
className: "mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
2219
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2220
|
+
className: "min-w-0 flex-1",
|
|
2221
|
+
children: [title && /* @__PURE__ */ jsx("h1", {
|
|
2222
|
+
className: "text-2xl md:text-3xl font-extrabold tracking-tight",
|
|
2223
|
+
children: title
|
|
2224
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
2225
|
+
className: "mt-1 text-muted-foreground",
|
|
2226
|
+
children: description
|
|
2227
|
+
})]
|
|
2228
|
+
}), actions && actions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
2229
|
+
className: "flex items-center gap-2 shrink-0",
|
|
2230
|
+
children: actions.map((action) => {
|
|
2231
|
+
const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
|
|
2232
|
+
const variant = action.variant || "default";
|
|
2233
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
2234
|
+
variant: variant === "primary" ? "default" : variant,
|
|
2235
|
+
size: "sm",
|
|
2236
|
+
onClick: () => handleActionClick(action),
|
|
2237
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 mr-2" }), resolveText(action.label)]
|
|
2238
|
+
}, action.id);
|
|
2239
|
+
})
|
|
2240
|
+
})]
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
function LayoutItemRenderer({ item, index, columns, basePath, navigate, widgetRegistry, resolveText }) {
|
|
2244
|
+
if (isWidgetConfig(item)) return /* @__PURE__ */ jsx("div", {
|
|
2245
|
+
className: cn("min-h-0 h-full", getSpanClass(item.span)),
|
|
2246
|
+
children: /* @__PURE__ */ jsx(DashboardWidget, {
|
|
2247
|
+
config: item,
|
|
2248
|
+
basePath,
|
|
2249
|
+
navigate,
|
|
2250
|
+
widgetRegistry
|
|
2251
|
+
})
|
|
2252
|
+
});
|
|
2253
|
+
if (isSectionConfig(item)) return /* @__PURE__ */ jsx(SectionRenderer, {
|
|
2254
|
+
section: item,
|
|
2255
|
+
basePath,
|
|
2256
|
+
navigate,
|
|
2257
|
+
widgetRegistry,
|
|
2258
|
+
resolveText
|
|
2259
|
+
}, `section-${index}`);
|
|
2260
|
+
if (isTabsConfig(item)) return /* @__PURE__ */ jsx(TabsRenderer, {
|
|
2261
|
+
tabs: item,
|
|
2262
|
+
basePath,
|
|
2263
|
+
navigate,
|
|
2264
|
+
widgetRegistry,
|
|
2265
|
+
resolveText
|
|
2266
|
+
}, `tabs-${index}`);
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
function SectionRenderer({ section, basePath, navigate, widgetRegistry, resolveText }) {
|
|
2270
|
+
const { label, description, wrapper = "flat", defaultCollapsed = false, layout = "grid", columns = 4, gap, items, className } = section;
|
|
2271
|
+
const sectionLabel = label ? resolveText(label) : void 0;
|
|
2272
|
+
const sectionDescription = description ? resolveText(description) : void 0;
|
|
2273
|
+
const itemsContent = /* @__PURE__ */ jsx("div", {
|
|
2274
|
+
className: cn("@container", layout === "grid" && "grid gap-4 items-stretch", layout === "grid" && getGridClass(columns), layout === "stack" && "flex flex-col gap-4"),
|
|
2275
|
+
style: gap ? { gap: `${gap * .25}rem` } : void 0,
|
|
2276
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
|
|
2277
|
+
item,
|
|
2278
|
+
index,
|
|
2279
|
+
columns,
|
|
2280
|
+
basePath,
|
|
2281
|
+
navigate,
|
|
2282
|
+
widgetRegistry,
|
|
2283
|
+
resolveText
|
|
2284
|
+
}, getLayoutItemKey(item, index)))
|
|
2285
|
+
});
|
|
2286
|
+
if (wrapper === "flat") return /* @__PURE__ */ jsxs("div", {
|
|
2287
|
+
className: cn("col-span-full", className),
|
|
2288
|
+
children: [(sectionLabel || sectionDescription) && /* @__PURE__ */ jsxs("div", {
|
|
2289
|
+
className: "mb-4",
|
|
2290
|
+
children: [sectionLabel && /* @__PURE__ */ jsx("h2", {
|
|
2291
|
+
className: "text-lg font-semibold",
|
|
2292
|
+
children: sectionLabel
|
|
2293
|
+
}), sectionDescription && /* @__PURE__ */ jsx("p", {
|
|
2294
|
+
className: "text-sm text-muted-foreground mt-1",
|
|
2295
|
+
children: sectionDescription
|
|
2296
|
+
})]
|
|
2297
|
+
}), itemsContent]
|
|
2298
|
+
});
|
|
2299
|
+
if (wrapper === "card") return /* @__PURE__ */ jsxs(Card, {
|
|
2300
|
+
className: cn("col-span-full", className),
|
|
2301
|
+
children: [(sectionLabel || sectionDescription) && /* @__PURE__ */ jsxs(CardHeader, { children: [sectionLabel && /* @__PURE__ */ jsx(CardTitle, { children: sectionLabel }), sectionDescription && /* @__PURE__ */ jsx("p", {
|
|
2302
|
+
className: "text-sm text-muted-foreground",
|
|
2303
|
+
children: sectionDescription
|
|
2304
|
+
})] }), /* @__PURE__ */ jsx(CardContent, { children: itemsContent })]
|
|
2305
|
+
});
|
|
2306
|
+
if (wrapper === "collapsible") return /* @__PURE__ */ jsx(Accordion$1, {
|
|
2307
|
+
defaultValue: defaultCollapsed ? [] : [0],
|
|
2308
|
+
className: cn("col-span-full", className),
|
|
2309
|
+
children: /* @__PURE__ */ jsxs(AccordionItem, {
|
|
2310
|
+
className: "border-none",
|
|
2311
|
+
children: [/* @__PURE__ */ jsx(AccordionTrigger, {
|
|
2312
|
+
className: "hover:no-underline py-2",
|
|
2313
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2314
|
+
className: "text-left",
|
|
2315
|
+
children: [sectionLabel && /* @__PURE__ */ jsx("span", {
|
|
2316
|
+
className: "text-lg font-semibold",
|
|
2317
|
+
children: sectionLabel
|
|
2318
|
+
}), sectionDescription && /* @__PURE__ */ jsx("p", {
|
|
2319
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
2320
|
+
children: sectionDescription
|
|
2321
|
+
})]
|
|
2322
|
+
})
|
|
2323
|
+
}), /* @__PURE__ */ jsx(AccordionContent, {
|
|
2324
|
+
className: "pt-4",
|
|
2325
|
+
children: itemsContent
|
|
2326
|
+
})]
|
|
2327
|
+
})
|
|
2328
|
+
});
|
|
2329
|
+
return itemsContent;
|
|
2330
|
+
}
|
|
2331
|
+
function TabsRenderer({ tabs, basePath, navigate, widgetRegistry, resolveText }) {
|
|
2332
|
+
const { tabs: tabConfigs, defaultTab, variant = "default" } = tabs;
|
|
2333
|
+
return /* @__PURE__ */ jsxs(Tabs$1, {
|
|
2334
|
+
defaultValue: defaultTab || tabConfigs[0]?.id,
|
|
2335
|
+
className: "col-span-full",
|
|
2336
|
+
children: [/* @__PURE__ */ jsx(TabsList, {
|
|
2337
|
+
variant: variant === "line" ? "line" : "default",
|
|
2338
|
+
className: "mb-4",
|
|
2339
|
+
children: tabConfigs.map((tab) => /* @__PURE__ */ jsxs(TabsTrigger, {
|
|
2340
|
+
value: tab.id,
|
|
2341
|
+
children: [
|
|
2342
|
+
tab.icon && /* @__PURE__ */ jsx(tab.icon, { className: "h-4 w-4 mr-2" }),
|
|
2343
|
+
resolveText(tab.label),
|
|
2344
|
+
tab.badge !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
2345
|
+
className: "ml-2 rounded-full bg-muted px-2 py-0.5 text-xs",
|
|
2346
|
+
children: tab.badge
|
|
2347
|
+
})
|
|
2348
|
+
]
|
|
2349
|
+
}, tab.id))
|
|
2350
|
+
}), tabConfigs.map((tab) => /* @__PURE__ */ jsx(TabsContent, {
|
|
2351
|
+
value: tab.id,
|
|
2352
|
+
children: /* @__PURE__ */ jsx(TabContentRenderer, {
|
|
2353
|
+
tab,
|
|
2354
|
+
basePath,
|
|
2355
|
+
navigate,
|
|
2356
|
+
widgetRegistry,
|
|
2357
|
+
resolveText
|
|
2358
|
+
})
|
|
2359
|
+
}, tab.id))]
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
function TabContentRenderer({ tab, basePath, navigate, widgetRegistry, resolveText }) {
|
|
2363
|
+
const columns = 4;
|
|
2364
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2365
|
+
className: cn("@container grid gap-4 items-stretch", getGridClass(columns)),
|
|
2366
|
+
children: tab.items.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
|
|
2367
|
+
item,
|
|
2368
|
+
index,
|
|
2369
|
+
columns,
|
|
2370
|
+
basePath,
|
|
2371
|
+
navigate,
|
|
2372
|
+
widgetRegistry,
|
|
2373
|
+
resolveText
|
|
2374
|
+
}, getLayoutItemKey(item, index)))
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* DashboardGrid - Renders a configurable grid of dashboard widgets
|
|
2379
|
+
*
|
|
2380
|
+
* @example
|
|
2381
|
+
* ```tsx
|
|
2382
|
+
* const dashboardConfig: DashboardConfig = {
|
|
2383
|
+
* title: "Dashboard",
|
|
2384
|
+
* description: "Welcome to your admin dashboard",
|
|
2385
|
+
* columns: 4,
|
|
2386
|
+
* items: [
|
|
2387
|
+
* { type: "stats", collection: "posts", label: "Total Posts", span: 1 },
|
|
2388
|
+
* { type: "stats", collection: "users", label: "Total Users", span: 1 },
|
|
2389
|
+
* {
|
|
2390
|
+
* type: "section",
|
|
2391
|
+
* label: "Analytics",
|
|
2392
|
+
* wrapper: "card",
|
|
2393
|
+
* items: [
|
|
2394
|
+
* { type: "chart", collection: "posts", field: "createdAt", span: 2 },
|
|
2395
|
+
* ]
|
|
2396
|
+
* },
|
|
2397
|
+
* {
|
|
2398
|
+
* type: "tabs",
|
|
2399
|
+
* tabs: [
|
|
2400
|
+
* { id: "recent", label: "Recent", items: [...] },
|
|
2401
|
+
* { id: "popular", label: "Popular", items: [...] },
|
|
2402
|
+
* ]
|
|
2403
|
+
* }
|
|
2404
|
+
* ],
|
|
2405
|
+
* };
|
|
2406
|
+
*
|
|
2407
|
+
* <DashboardGrid config={dashboardConfig} basePath="/admin" navigate={navigate} />
|
|
2408
|
+
* ```
|
|
2409
|
+
*/
|
|
2410
|
+
function DashboardGrid({ config, basePath = "/admin", navigate, widgetRegistry, className }) {
|
|
2411
|
+
const resolveText = useResolveText();
|
|
2412
|
+
const { title, description, columns = 4 } = config;
|
|
2413
|
+
const layoutItems = config.items || config.widgets || [];
|
|
2414
|
+
const resolvedTitle = title ? resolveText(title) : void 0;
|
|
2415
|
+
const resolvedDescription = description ? resolveText(description) : void 0;
|
|
2416
|
+
if (layoutItems.length === 0) return /* @__PURE__ */ jsxs("div", {
|
|
2417
|
+
className: cn("@container", className),
|
|
2418
|
+
children: [/* @__PURE__ */ jsx(DashboardHeader, {
|
|
2419
|
+
title: resolvedTitle,
|
|
2420
|
+
description: resolvedDescription,
|
|
2421
|
+
actions: config.actions,
|
|
2422
|
+
navigate,
|
|
2423
|
+
resolveText
|
|
2424
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2425
|
+
className: "flex h-64 items-center justify-center border border-dashed border-border bg-card/50 rounded-lg",
|
|
2426
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2427
|
+
className: "text-center",
|
|
2428
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2429
|
+
className: "text-muted-foreground font-medium",
|
|
2430
|
+
children: "No widgets configured"
|
|
2431
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
2432
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
2433
|
+
children: "Add widgets to your dashboard configuration to display data here."
|
|
2434
|
+
})]
|
|
2435
|
+
})
|
|
2436
|
+
})]
|
|
2437
|
+
});
|
|
2438
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2439
|
+
className: cn("@container", className),
|
|
2440
|
+
children: [/* @__PURE__ */ jsx(DashboardHeader, {
|
|
2441
|
+
title: resolvedTitle,
|
|
2442
|
+
description: resolvedDescription,
|
|
2443
|
+
actions: config.actions,
|
|
2444
|
+
navigate,
|
|
2445
|
+
resolveText
|
|
2446
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2447
|
+
className: cn("grid gap-4 items-stretch", getGridClass(columns)),
|
|
2448
|
+
children: layoutItems.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
|
|
2449
|
+
item,
|
|
2450
|
+
index,
|
|
2451
|
+
columns,
|
|
2452
|
+
basePath,
|
|
2453
|
+
navigate,
|
|
2454
|
+
widgetRegistry,
|
|
2455
|
+
resolveText
|
|
2456
|
+
}, getLayoutItemKey(item, index)))
|
|
2457
|
+
})]
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
//#endregion
|
|
2462
|
+
//#region src/client/views/pages/dashboard-page.tsx
|
|
2463
|
+
/**
|
|
2464
|
+
* Default dashboard page component.
|
|
2465
|
+
*
|
|
2466
|
+
* Reads dashboard configuration from AdminProvider and renders DashboardGrid.
|
|
2467
|
+
*
|
|
2468
|
+
* @example
|
|
2469
|
+
* ```tsx
|
|
2470
|
+
* // In your admin config
|
|
2471
|
+
* const admin = qa<AppCMS>()
|
|
2472
|
+
* .use(coreAdminModule)
|
|
2473
|
+
* .dashboard({
|
|
2474
|
+
* title: "Welcome",
|
|
2475
|
+
* widgets: [
|
|
2476
|
+
* { type: "stats", collection: "posts" },
|
|
2477
|
+
* ],
|
|
2478
|
+
* })
|
|
2479
|
+
* ```
|
|
2480
|
+
*/
|
|
2481
|
+
function DashboardPage({ title, description, className }) {
|
|
2482
|
+
const admin = useAdminStore(selectAdmin);
|
|
2483
|
+
const basePath = useAdminStore(selectBasePath);
|
|
2484
|
+
const navigate = useAdminStore(selectNavigate);
|
|
2485
|
+
return /* @__PURE__ */ jsx(DashboardGrid, {
|
|
2486
|
+
config: {
|
|
2487
|
+
...admin.getDashboard(),
|
|
2488
|
+
...title && { title },
|
|
2489
|
+
...description && { description }
|
|
2490
|
+
},
|
|
2491
|
+
basePath,
|
|
2492
|
+
navigate,
|
|
2493
|
+
className
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
var dashboard_page_default = DashboardPage;
|
|
2497
|
+
|
|
2498
|
+
//#endregion
|
|
2499
|
+
export { EmailCell as C, TextCell as D, SelectCell as E, TimeCell as O, DefaultCell as S, RichTextCell as T, AccordionItem as _, Skeleton as a, DateCell as b, useCollectionItem as c, Tabs$1 as d, TabsContent as f, AccordionContent as g, Accordion$1 as h, DashboardWidget as i, Badge as k, useCollectionList as l, TabsTrigger as m, dashboard_page_default as n, useCollectionCreate as o, TabsList as p, DashboardGrid as r, useCollectionDelete as s, DashboardPage as t, useCollectionUpdate as u, AccordionTrigger as v, NumberCell as w, DateTimeCell as x, BooleanCell as y };
|
|
2500
|
+
//# sourceMappingURL=dashboard-page-B4PGEdc2.mjs.map
|