@ttoss/react-dashboard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Terezinha Tech Operations (ttoss)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,539 @@
1
+ # @ttoss/react-dashboard
2
+
3
+ ## About
4
+
5
+ A comprehensive React dashboard module that provides fully customizable dashboard functionality with filters, templates, and responsive grid layouts. This module enables you to create data-rich dashboards with support for multiple card types, filter systems, and template management.
6
+
7
+ ## Installation
8
+
9
+ ```shell
10
+ pnpm add @ttoss/react-dashboard @ttoss/components @ttoss/react-icons @ttoss/ui @emotion/react react-day-picker react-grid-layout
11
+ ```
12
+
13
+ ## Getting Started
14
+
15
+ ### Provider Setup
16
+
17
+ If you're using the `Dashboard` component directly, you don't need to set up the provider separately as it wraps the `DashboardProvider` internally. However, if you need to use the `useDashboard` hook in other components, you can wrap your application with the `DashboardProvider`:
18
+
19
+ ```tsx
20
+ import { DashboardProvider } from '@ttoss/react-dashboard';
21
+ import { ThemeProvider } from '@ttoss/ui';
22
+ import type {
23
+ DashboardTemplate,
24
+ DashboardFilter,
25
+ } from '@ttoss/react-dashboard';
26
+
27
+ const templates: DashboardTemplate[] = [];
28
+ const filters: DashboardFilter[] = [];
29
+
30
+ ReactDOM.render(
31
+ <React.StrictMode>
32
+ <ThemeProvider>
33
+ <DashboardProvider filters={filters} templates={templates}>
34
+ <App />
35
+ </DashboardProvider>
36
+ </ThemeProvider>
37
+ </React.StrictMode>,
38
+ document.getElementById('root')
39
+ );
40
+ ```
41
+
42
+ ### Basic Dashboard Usage
43
+
44
+ ```tsx
45
+ import { Dashboard } from '@ttoss/react-dashboard';
46
+ import type {
47
+ DashboardTemplate,
48
+ DashboardFilter,
49
+ } from '@ttoss/react-dashboard';
50
+ import { DashboardFilterType } from '@ttoss/react-dashboard';
51
+
52
+ const MyDashboard = () => {
53
+ const templates: DashboardTemplate[] = [
54
+ {
55
+ id: 'default',
56
+ name: 'Default Template',
57
+ description: 'My default dashboard layout',
58
+ grid: [
59
+ {
60
+ i: 'card-1',
61
+ x: 0,
62
+ y: 0,
63
+ w: 4,
64
+ h: 4,
65
+ card: {
66
+ title: 'Total Revenue',
67
+ numberType: 'currency',
68
+ type: 'bigNumber',
69
+ sourceType: [{ source: 'api' }],
70
+ data: {
71
+ api: { total: 150000 },
72
+ },
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ ];
78
+
79
+ const filters: DashboardFilter[] = [
80
+ {
81
+ key: 'date-range',
82
+ type: DashboardFilterType.DATE_RANGE,
83
+ label: 'Date Range',
84
+ value: { from: new Date(), to: new Date() },
85
+ },
86
+ ];
87
+
88
+ return <Dashboard templates={templates} filters={filters} loading={false} />;
89
+ };
90
+ ```
91
+
92
+ ## Components
93
+
94
+ ### Dashboard
95
+
96
+ The main dashboard component that orchestrates the entire dashboard experience.
97
+
98
+ ```tsx
99
+ import { Dashboard } from '@ttoss/react-dashboard';
100
+
101
+ <Dashboard
102
+ templates={templates}
103
+ filters={filters}
104
+ loading={false}
105
+ headerChildren={<CustomHeaderContent />}
106
+ onFiltersChange={(filters) => {
107
+ // Handle filter changes
108
+ }}
109
+ />;
110
+ ```
111
+
112
+ **Props:**
113
+
114
+ | Prop | Type | Default | Description |
115
+ | ----------------- | -------------------------------------- | ------- | ------------------------------------------ |
116
+ | `templates` | `DashboardTemplate[]` | `[]` | Array of dashboard templates |
117
+ | `filters` | `DashboardFilter[]` | `[]` | Array of dashboard filters |
118
+ | `loading` | `boolean` | `false` | Loading state for the dashboard |
119
+ | `headerChildren` | `React.ReactNode` | - | Additional content to render in the header |
120
+ | `onFiltersChange` | `(filters: DashboardFilter[]) => void` | - | Callback when filters change |
121
+
122
+ ### DashboardProvider
123
+
124
+ Context provider that manages dashboard state (filters and templates).
125
+
126
+ ```tsx
127
+ import { DashboardProvider } from '@ttoss/react-dashboard';
128
+
129
+ <DashboardProvider
130
+ filters={filters}
131
+ templates={templates}
132
+ onFiltersChange={handleFiltersChange}
133
+ >
134
+ {children}
135
+ </DashboardProvider>;
136
+ ```
137
+
138
+ **Props:**
139
+
140
+ | Prop | Type | Default | Description |
141
+ | ----------------- | -------------------------------------- | ------- | ---------------------------- |
142
+ | `children` | `React.ReactNode` | - | Child components |
143
+ | `filters` | `DashboardFilter[]` | `[]` | Filter state |
144
+ | `templates` | `DashboardTemplate[]` | `[]` | Template state |
145
+ | `onFiltersChange` | `(filters: DashboardFilter[]) => void` | - | Callback when filters change |
146
+
147
+ ### useDashboard Hook
148
+
149
+ Hook to access and modify dashboard state.
150
+
151
+ ```tsx
152
+ import { useDashboard } from '@ttoss/react-dashboard';
153
+
154
+ const MyComponent = () => {
155
+ const { filters, updateFilter, templates, selectedTemplate } = useDashboard();
156
+
157
+ // Use dashboard state
158
+ const handleFilterChange = (key: string, value: DashboardFilterValue) => {
159
+ updateFilter(key, value);
160
+ };
161
+ };
162
+ ```
163
+
164
+ **Returns:**
165
+
166
+ | Property | Type | Description |
167
+ | ------------------ | ---------------------------------------------------- | -------------------------------------------- |
168
+ | `filters` | `DashboardFilter[]` | Current filter state |
169
+ | `updateFilter` | `(key: string, value: DashboardFilterValue) => void` | Function to update a specific filter by key |
170
+ | `templates` | `DashboardTemplate[]` | Current template state |
171
+ | `selectedTemplate` | `DashboardTemplate \| undefined` | Currently selected template based on filters |
172
+
173
+ ### DashboardCard
174
+
175
+ Component for rendering individual dashboard cards. Currently supports `bigNumber` type.
176
+
177
+ ```tsx
178
+ import { DashboardCard } from '@ttoss/react-dashboard';
179
+
180
+ <DashboardCard
181
+ title="Total Revenue"
182
+ description="Revenue from all sources"
183
+ numberType="currency"
184
+ type="bigNumber"
185
+ sourceType={[{ source: 'api' }]}
186
+ data={{
187
+ api: { total: 150000 },
188
+ }}
189
+ trend={{
190
+ value: 15.5,
191
+ status: 'positive',
192
+ }}
193
+ />;
194
+ ```
195
+
196
+ **Props:**
197
+
198
+ | Prop | Type | Default | Description |
199
+ | ---------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------- |
200
+ | `title` | `string` | - | Card title |
201
+ | `description` | `string` | - | Optional card description |
202
+ | `icon` | `string` | - | Optional icon name |
203
+ | `color` | `string` | - | Optional color for the card |
204
+ | `variant` | `CardVariant` | - | Card variant (`'default' \| 'dark' \| 'light-green'`) |
205
+ | `numberType` | `CardNumberType` | - | Number formatting type (`'number' \| 'percentage' \| 'currency'`) |
206
+ | `type` | `DashboardCardType` | - | Card type (`'bigNumber' \| 'pieChart' \| 'barChart' \| 'lineChart' \| 'table' \| 'list'`) |
207
+ | `sourceType` | `CardSourceType[]` | - | Data source configuration |
208
+ | `labels` | `Array<string \| number>` | - | Optional labels for the card |
209
+ | `data` | `DashboardCardData` | - | Card data from various sources |
210
+ | `trend` | `TrendIndicator` | - | Optional trend indicator |
211
+ | `additionalInfo` | `string` | - | Optional additional information text |
212
+ | `status` | `StatusIndicator` | - | Optional status indicator |
213
+
214
+ ### DashboardFilters
215
+
216
+ Component that automatically renders filters based on the dashboard state.
217
+
218
+ ```tsx
219
+ import { DashboardFilters } from '@ttoss/react-dashboard';
220
+
221
+ // Automatically renders filters from DashboardProvider context
222
+ <DashboardFilters />;
223
+ ```
224
+
225
+ This component reads filters from the `DashboardProvider` context and renders the appropriate filter component for each filter type.
226
+
227
+ ### DashboardHeader
228
+
229
+ Header component that displays filters and optional custom content.
230
+
231
+ ```tsx
232
+ import { DashboardHeader } from '@ttoss/react-dashboard';
233
+
234
+ <DashboardHeader>
235
+ <CustomActionButtons />
236
+ </DashboardHeader>;
237
+ ```
238
+
239
+ **Props:**
240
+
241
+ | Prop | Type | Description |
242
+ | ---------- | ----------------- | ------------------------------------ |
243
+ | `children` | `React.ReactNode` | Optional content to render in header |
244
+
245
+ ### DashboardGrid
246
+
247
+ Responsive grid layout component that displays dashboard cards using `react-grid-layout`.
248
+
249
+ ```tsx
250
+ import { DashboardGrid } from '@ttoss/react-dashboard';
251
+
252
+ <DashboardGrid loading={false} />;
253
+ ```
254
+
255
+ **Props:**
256
+
257
+ | Prop | Type | Default | Description |
258
+ | --------- | --------- | ------- | ----------------------------- |
259
+ | `loading` | `boolean` | - | Shows loading spinner if true |
260
+
261
+ ## Filter Types
262
+
263
+ ### Text Filter
264
+
265
+ A text input filter for string values.
266
+
267
+ ```tsx
268
+ import { DashboardFilterType } from '@ttoss/react-dashboard';
269
+
270
+ const textFilter: DashboardFilter = {
271
+ key: 'search',
272
+ type: DashboardFilterType.TEXT,
273
+ label: 'Search',
274
+ placeholder: 'Enter search term...',
275
+ value: '',
276
+ onChange: (value) => {
277
+ console.log('Search:', value);
278
+ },
279
+ };
280
+ ```
281
+
282
+ ### Select Filter
283
+
284
+ A dropdown select filter for predefined options.
285
+
286
+ ```tsx
287
+ const selectFilter: DashboardFilter = {
288
+ key: 'status',
289
+ type: DashboardFilterType.SELECT,
290
+ label: 'Status',
291
+ value: 'active',
292
+ options: [
293
+ { label: 'Active', value: 'active' },
294
+ { label: 'Inactive', value: 'inactive' },
295
+ ],
296
+ onChange: (value) => {
297
+ console.log('Status:', value);
298
+ },
299
+ };
300
+ ```
301
+
302
+ ### Date Range Filter
303
+
304
+ A date range picker with optional presets.
305
+
306
+ ```tsx
307
+ const dateRangeFilter: DashboardFilter = {
308
+ key: 'date-range',
309
+ type: DashboardFilterType.DATE_RANGE,
310
+ label: 'Date Range',
311
+ value: { from: new Date(), to: new Date() },
312
+ presets: [
313
+ {
314
+ label: 'Last 7 days',
315
+ getValue: () => ({
316
+ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
317
+ to: new Date(),
318
+ }),
319
+ },
320
+ {
321
+ label: 'Last 30 days',
322
+ getValue: () => ({
323
+ from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
324
+ to: new Date(),
325
+ }),
326
+ },
327
+ ],
328
+ onChange: (value) => {
329
+ console.log('Date range:', value);
330
+ },
331
+ };
332
+ ```
333
+
334
+ ## Types
335
+
336
+ ### DashboardTemplate
337
+
338
+ ```tsx
339
+ interface DashboardTemplate {
340
+ id: string;
341
+ name: string;
342
+ description?: string;
343
+ grid: DashboardGridItem[];
344
+ }
345
+ ```
346
+
347
+ ### DashboardGridItem
348
+
349
+ ```tsx
350
+ interface DashboardGridItem extends ReactGridLayout.Layout {
351
+ card: DashboardCard;
352
+ }
353
+ ```
354
+
355
+ ### DashboardFilter
356
+
357
+ ```tsx
358
+ interface DashboardFilter {
359
+ key: string;
360
+ type: DashboardFilterType;
361
+ label: string;
362
+ placeholder?: string;
363
+ value: DashboardFilterValue;
364
+ onChange?: (value: DashboardFilterValue) => void;
365
+ options?: { label: string; value: string | number | boolean }[];
366
+ presets?: { label: string; getValue: () => DateRange }[];
367
+ }
368
+ ```
369
+
370
+ ### DashboardFilterValue
371
+
372
+ ```tsx
373
+ type DashboardFilterValue =
374
+ | string
375
+ | number
376
+ | boolean
377
+ | { from: Date; to: Date };
378
+ ```
379
+
380
+ ### DashboardFilterType
381
+
382
+ ```tsx
383
+ enum DashboardFilterType {
384
+ TEXT = 'text',
385
+ SELECT = 'select',
386
+ DATE_RANGE = 'date-range',
387
+ NUMBER = 'number',
388
+ BOOLEAN = 'boolean',
389
+ }
390
+ ```
391
+
392
+ ### DashboardCard
393
+
394
+ See [DashboardCard](#dashboardcard) section for full interface.
395
+
396
+ ### CardNumberType
397
+
398
+ ```tsx
399
+ type CardNumberType = 'number' | 'percentage' | 'currency';
400
+ ```
401
+
402
+ ### CardSourceType
403
+
404
+ ```tsx
405
+ type CardSourceType = {
406
+ source: 'meta' | 'oneclickads' | 'api';
407
+ level?: 'adAccount' | 'campaign' | 'adSet' | 'ad';
408
+ };
409
+ ```
410
+
411
+ ### DashboardCardType
412
+
413
+ ```tsx
414
+ type DashboardCardType =
415
+ | 'bigNumber'
416
+ | 'pieChart'
417
+ | 'barChart'
418
+ | 'lineChart'
419
+ | 'table'
420
+ | 'list';
421
+ ```
422
+
423
+ ### CardVariant
424
+
425
+ ```tsx
426
+ type CardVariant = 'default' | 'dark' | 'light-green';
427
+ ```
428
+
429
+ ## Complete Example
430
+
431
+ ```tsx
432
+ import { Dashboard } from '@ttoss/react-dashboard';
433
+ import type {
434
+ DashboardTemplate,
435
+ DashboardFilter,
436
+ } from '@ttoss/react-dashboard';
437
+ import { DashboardFilterType } from '@ttoss/react-dashboard';
438
+
439
+ const App = () => {
440
+ const templates: DashboardTemplate[] = [
441
+ {
442
+ id: 'default',
443
+ name: 'Default Dashboard',
444
+ grid: [
445
+ {
446
+ i: 'revenue',
447
+ x: 0,
448
+ y: 0,
449
+ w: 4,
450
+ h: 4,
451
+ card: {
452
+ title: 'Total Revenue',
453
+ numberType: 'currency',
454
+ type: 'bigNumber',
455
+ sourceType: [{ source: 'api' }],
456
+ data: {
457
+ api: { total: 150000 },
458
+ },
459
+ trend: {
460
+ value: 15.5,
461
+ status: 'positive',
462
+ },
463
+ },
464
+ },
465
+ {
466
+ i: 'roas',
467
+ x: 4,
468
+ y: 0,
469
+ w: 4,
470
+ h: 4,
471
+ card: {
472
+ title: 'ROAS',
473
+ numberType: 'number',
474
+ type: 'bigNumber',
475
+ sourceType: [{ source: 'api' }],
476
+ data: {
477
+ api: { total: 3.5 },
478
+ },
479
+ variant: 'light-green',
480
+ },
481
+ },
482
+ ],
483
+ },
484
+ ];
485
+
486
+ const filters: DashboardFilter[] = [
487
+ {
488
+ key: 'template',
489
+ type: DashboardFilterType.SELECT,
490
+ label: 'Template',
491
+ value: 'default',
492
+ options: [{ label: 'Default Dashboard', value: 'default' }],
493
+ },
494
+ {
495
+ key: 'date-range',
496
+ type: DashboardFilterType.DATE_RANGE,
497
+ label: 'Date Range',
498
+ value: {
499
+ from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
500
+ to: new Date(),
501
+ },
502
+ presets: [
503
+ {
504
+ label: 'Last 7 days',
505
+ getValue: () => ({
506
+ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
507
+ to: new Date(),
508
+ }),
509
+ },
510
+ {
511
+ label: 'Last 30 days',
512
+ getValue: () => ({
513
+ from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
514
+ to: new Date(),
515
+ }),
516
+ },
517
+ ],
518
+ },
519
+ ];
520
+
521
+ const handleFiltersChange = (updatedFilters: DashboardFilter[]) => {
522
+ console.log('Filters changed:', updatedFilters);
523
+ // Update your data fetching logic here
524
+ };
525
+
526
+ return (
527
+ <Dashboard
528
+ templates={templates}
529
+ filters={filters}
530
+ loading={false}
531
+ onFiltersChange={handleFiltersChange}
532
+ />
533
+ );
534
+ };
535
+ ```
536
+
537
+ ## License
538
+
539
+ [MIT](https://github.com/ttoss/ttoss/blob/main/LICENSE)
@@ -0,0 +1,611 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import * as React from 'react';
3
+ var __defProp = Object.defineProperty;
4
+ var __name = (target, value) => __defProp(target, "name", {
5
+ value,
6
+ configurable: true
7
+ });
8
+
9
+ // src/Dashboard.tsx
10
+ import { Divider, Flex as Flex6 } from "@ttoss/ui";
11
+ import * as React8 from "react";
12
+
13
+ // src/DashboardGrid.tsx
14
+ import "react-grid-layout/css/styles.css";
15
+ import { Box as Box3, Flex as Flex3, Spinner } from "@ttoss/ui";
16
+ import { Responsive, WidthProvider } from "react-grid-layout";
17
+
18
+ // src/Cards/BigNumber.tsx
19
+ import { Icon } from "@ttoss/react-icons";
20
+ import { Box as Box2, Flex as Flex2, Text as Text2 } from "@ttoss/ui";
21
+
22
+ // src/Cards/Wrapper.tsx
23
+ import { Box, Flex, Text, TooltipIcon } from "@ttoss/ui";
24
+ var CardWrapper = /* @__PURE__ */__name(({
25
+ title,
26
+ children,
27
+ description,
28
+ variant = "default"
29
+ }) => {
30
+ const getBackgroundColor = /* @__PURE__ */__name(() => {
31
+ switch (variant) {
32
+ case "dark":
33
+ return "input.background.secondary.default";
34
+ case "light-green":
35
+ return "feedback.background.positive.default";
36
+ case "default":
37
+ default:
38
+ return "display.background.primary.default";
39
+ }
40
+ }, "getBackgroundColor");
41
+ const getTitleColor = /* @__PURE__ */__name(() => {
42
+ switch (variant) {
43
+ case "dark":
44
+ return "action.text.primary.default";
45
+ case "light-green":
46
+ return "feedback.text.positive.default";
47
+ case "default":
48
+ default:
49
+ return "display.text.primary.default";
50
+ }
51
+ }, "getTitleColor");
52
+ return /* @__PURE__ */React.createElement(Flex, {
53
+ sx: {
54
+ flexDirection: "column",
55
+ gap: "3",
56
+ backgroundColor: getBackgroundColor(),
57
+ borderRadius: "lg",
58
+ padding: "4",
59
+ width: "100%",
60
+ height: "100%",
61
+ boxShadow: "0px 2px 8px rgba(0, 0, 0, 0.15)"
62
+ }
63
+ }, /* @__PURE__ */React.createElement(Box, {
64
+ sx: {
65
+ alignItems: "center",
66
+ gap: "2"
67
+ }
68
+ }, /* @__PURE__ */React.createElement(Text, {
69
+ variant: "h5",
70
+ sx: {
71
+ color: getTitleColor(),
72
+ flex: 1,
73
+ minWidth: 0
74
+ }
75
+ }, title), description && /* @__PURE__ */React.createElement(TooltipIcon, {
76
+ variant: "info",
77
+ icon: "ant-design:info-circle-outlined",
78
+ tooltip: {
79
+ children: description
80
+ },
81
+ sx: {
82
+ marginLeft: "2",
83
+ flexShrink: 0
84
+ }
85
+ })), /* @__PURE__ */React.createElement(Flex, {
86
+ sx: {
87
+ flexDirection: "column",
88
+ flex: "auto",
89
+ height: "100%",
90
+ justifyContent: "center"
91
+ }
92
+ }, children));
93
+ }, "CardWrapper");
94
+
95
+ // src/Cards/BigNumber.tsx
96
+ var formatNumber = /* @__PURE__ */__name((value, type) => {
97
+ if (value === void 0 || value === null) {
98
+ return "-";
99
+ }
100
+ switch (type) {
101
+ case "currency":
102
+ return new Intl.NumberFormat("pt-BR", {
103
+ style: "currency",
104
+ currency: "BRL"
105
+ }).format(value);
106
+ case "percentage":
107
+ return `${value.toFixed(2)}%`;
108
+ case "number":
109
+ default:
110
+ {
111
+ const formatted = new Intl.NumberFormat("pt-BR", {
112
+ minimumFractionDigits: 2,
113
+ maximumFractionDigits: 2
114
+ }).format(value);
115
+ return formatted;
116
+ }
117
+ }
118
+ }, "formatNumber");
119
+ var getValueColor = /* @__PURE__ */__name((color, variant) => {
120
+ if (variant === "dark") {
121
+ return "action.text.primary.default";
122
+ }
123
+ if (variant === "light-green") {
124
+ return "feedback.text.positive.default";
125
+ }
126
+ if (!color) {
127
+ return "display.text.primary.default";
128
+ }
129
+ if (["green", "accent", "positive"].includes(color.toLowerCase())) {
130
+ return "display.text.accent.default";
131
+ }
132
+ return "display.text.primary.default";
133
+ }, "getValueColor");
134
+ var getTrendColor = /* @__PURE__ */__name(status => {
135
+ if (status === "positive") {
136
+ return "display.text.accent.default";
137
+ }
138
+ if (status === "negative") {
139
+ return "display.text.negative.default";
140
+ }
141
+ return "display.text.primary.default";
142
+ }, "getTrendColor");
143
+ var BigNumber = /* @__PURE__ */__name(props => {
144
+ const total = props.data.api?.total ?? props.data.meta?.total ?? void 0;
145
+ const formattedValue = formatNumber(total, props.numberType);
146
+ const displayValue = props.numberType === "number" && props.title.toLowerCase().includes("roas") ? `${formattedValue}x` : formattedValue;
147
+ const valueColor = getValueColor(props.color, props.variant);
148
+ const variant = props.variant || "default";
149
+ return /* @__PURE__ */React.createElement(CardWrapper, {
150
+ title: props.title,
151
+ description: props.description || "",
152
+ variant
153
+ }, /* @__PURE__ */React.createElement(Flex2, {
154
+ sx: {
155
+ flexDirection: "column",
156
+ gap: "2"
157
+ }
158
+ }, /* @__PURE__ */React.createElement(Text2, {
159
+ sx: {
160
+ color: valueColor,
161
+ fontSize: "2xl",
162
+ fontWeight: "bold",
163
+ lineHeight: "1.2"
164
+ }
165
+ }, displayValue), props.trend && /* @__PURE__ */React.createElement(Flex2, {
166
+ sx: {
167
+ alignItems: "center",
168
+ gap: "1"
169
+ }
170
+ }, /* @__PURE__ */React.createElement(Box2, {
171
+ sx: {
172
+ color: getTrendColor(props.trend.status),
173
+ display: "flex",
174
+ alignItems: "center"
175
+ }
176
+ }, props.trend.status === "positive" ? /* @__PURE__ */React.createElement(Icon, {
177
+ icon: "mdi:arrow-up",
178
+ width: 16
179
+ }) : props.trend.status === "negative" ? /* @__PURE__ */React.createElement(Icon, {
180
+ icon: "mdi:arrow-down",
181
+ width: 16
182
+ }) : null, props.trend.status === "neutral" ? /* @__PURE__ */React.createElement(Icon, {
183
+ icon: "mdi:arrow-right",
184
+ width: 16
185
+ }) : null), /* @__PURE__ */React.createElement(Text2, {
186
+ sx: {
187
+ color: getTrendColor(props.trend.status),
188
+ fontSize: "sm",
189
+ fontWeight: "medium"
190
+ }
191
+ }, props.trend.status === "positive" ? "+" : "", props.trend.status === "negative" ? "-" : "", props.trend.status === "neutral" ? "" : "", props.trend.value.toFixed(1), "%", " ", props.trend ? "vs. per\xEDodo anterior" : "")), props.additionalInfo && /* @__PURE__ */React.createElement(Text2, {
192
+ sx: {
193
+ color: getValueColor(props.color, props.variant),
194
+ fontSize: "sm",
195
+ mt: "1"
196
+ }
197
+ }, props.additionalInfo), props.status && /* @__PURE__ */React.createElement(Flex2, {
198
+ sx: {
199
+ alignItems: "center",
200
+ gap: "1",
201
+ mt: props.trend || props.additionalInfo ? "2" : "1"
202
+ }
203
+ }, props.status.icon && /* @__PURE__ */React.createElement(Box2, {
204
+ sx: {
205
+ color: "feedback.text.positive.default",
206
+ display: "flex",
207
+ alignItems: "center"
208
+ }
209
+ }, /* @__PURE__ */React.createElement(Icon, {
210
+ icon: props.status.icon,
211
+ width: 16
212
+ })), /* @__PURE__ */React.createElement(Text2, {
213
+ sx: {
214
+ color: "feedback.text.positive.default",
215
+ fontSize: "sm",
216
+ fontWeight: "medium"
217
+ }
218
+ }, props.status.text))));
219
+ }, "BigNumber");
220
+
221
+ // src/DashboardCard.tsx
222
+ var DashboardCard = /* @__PURE__ */__name(props => {
223
+ switch (props.type) {
224
+ case "bigNumber":
225
+ return /* @__PURE__ */React.createElement(BigNumber, props);
226
+ default:
227
+ return null;
228
+ }
229
+ }, "DashboardCard");
230
+
231
+ // src/DashboardGrid.tsx
232
+ var ResponsiveGridLayout = WidthProvider(Responsive);
233
+ var DashboardGrid = /* @__PURE__ */__name(({
234
+ loading,
235
+ selectedTemplate
236
+ }) => {
237
+ if (!selectedTemplate) {
238
+ return null;
239
+ }
240
+ const breakpoints = {
241
+ xs: 0,
242
+ sm: 480,
243
+ md: 768,
244
+ lg: 1024,
245
+ xl: 1280,
246
+ "2xl": 1536
247
+ };
248
+ const cols = {
249
+ xs: 2,
250
+ sm: 2,
251
+ md: 12,
252
+ lg: 12,
253
+ xl: 12,
254
+ "2xl": 12
255
+ };
256
+ const baseLayout = selectedTemplate.grid.map(item => {
257
+ const {
258
+ card,
259
+ ...layout
260
+ } = item;
261
+ return layout;
262
+ });
263
+ const layouts = {
264
+ xs: baseLayout,
265
+ sm: baseLayout,
266
+ md: baseLayout,
267
+ lg: baseLayout,
268
+ xl: baseLayout,
269
+ "2xl": baseLayout
270
+ };
271
+ return /* @__PURE__ */React.createElement(Box3, {
272
+ sx: {
273
+ width: "100%",
274
+ height: "full"
275
+ }
276
+ }, loading ? /* @__PURE__ */React.createElement(Flex3, {
277
+ sx: {
278
+ width: "100%",
279
+ height: "full",
280
+ justifyContent: "center",
281
+ alignItems: "center"
282
+ }
283
+ }, /* @__PURE__ */React.createElement(Spinner, null)) : /* @__PURE__ */React.createElement(ResponsiveGridLayout, {
284
+ className: "layout",
285
+ layouts,
286
+ breakpoints,
287
+ cols,
288
+ rowHeight: 30,
289
+ margin: [10, 10],
290
+ containerPadding: [0, 0]
291
+ }, selectedTemplate.grid.map(item => {
292
+ return /* @__PURE__ */React.createElement("div", {
293
+ key: item.i
294
+ }, /* @__PURE__ */React.createElement(DashboardCard, item.card));
295
+ })));
296
+ }, "DashboardGrid");
297
+
298
+ // src/DashboardHeader.tsx
299
+ import { Flex as Flex5 } from "@ttoss/ui";
300
+ import * as React7 from "react";
301
+
302
+ // src/DashboardFilters.tsx
303
+ import { Flex as Flex4 } from "@ttoss/ui";
304
+ import * as React6 from "react";
305
+
306
+ // src/DashboardProvider.tsx
307
+ import * as React2 from "react";
308
+ var DashboardContext = /* @__PURE__ */React2.createContext({
309
+ filters: [],
310
+ updateFilter: /* @__PURE__ */__name(() => {}, "updateFilter"),
311
+ templates: [],
312
+ selectedTemplate: void 0
313
+ });
314
+ var DashboardProvider = /* @__PURE__ */__name(props => {
315
+ const {
316
+ filters: externalFilters,
317
+ templates: externalTemplates,
318
+ onFiltersChange
319
+ } = props;
320
+ const onFiltersChangeRef = React2.useRef(onFiltersChange);
321
+ const filtersRef = React2.useRef(externalFilters);
322
+ React2.useEffect(() => {
323
+ onFiltersChangeRef.current = onFiltersChange;
324
+ }, [onFiltersChange]);
325
+ React2.useEffect(() => {
326
+ filtersRef.current = externalFilters;
327
+ }, [externalFilters]);
328
+ const updateFilter = React2.useCallback((key, value) => {
329
+ const updatedFilters = filtersRef.current.map(filter => {
330
+ return filter.key === key ? {
331
+ ...filter,
332
+ value
333
+ } : filter;
334
+ });
335
+ onFiltersChangeRef.current?.(updatedFilters);
336
+ }, []
337
+ // Empty deps - we use refs for current values
338
+ );
339
+ const selectedTemplate = React2.useMemo(() => {
340
+ const templateId = externalFilters.find(filter => {
341
+ return filter.key === "template";
342
+ })?.value;
343
+ return externalTemplates.find(template => {
344
+ return template.id === templateId;
345
+ });
346
+ }, [externalFilters, externalTemplates]);
347
+ return /* @__PURE__ */React2.createElement(DashboardContext.Provider, {
348
+ value: {
349
+ filters: externalFilters,
350
+ updateFilter,
351
+ templates: externalTemplates,
352
+ selectedTemplate
353
+ }
354
+ }, props.children);
355
+ }, "DashboardProvider");
356
+ var useDashboard = /* @__PURE__ */__name(() => {
357
+ const context = React2.useContext(DashboardContext);
358
+ if (!context) {
359
+ throw new Error("useDashboard must be used within a DashboardProvider");
360
+ }
361
+ return context;
362
+ }, "useDashboard");
363
+
364
+ // src/Filters/DateRangeFilter.tsx
365
+ import { FormFieldDatePicker } from "@ttoss/forms";
366
+ import * as React3 from "react";
367
+ import { FormProvider, useForm } from "react-hook-form";
368
+ var DateRangeFilter = /* @__PURE__ */__name(({
369
+ label,
370
+ value,
371
+ presets,
372
+ onChange
373
+ }) => {
374
+ const formMethods = useForm({
375
+ defaultValues: {
376
+ dateRange: value
377
+ },
378
+ mode: "onChange"
379
+ });
380
+ React3.useEffect(() => {
381
+ if (value !== void 0) {
382
+ formMethods.setValue("dateRange", value, {
383
+ shouldDirty: false
384
+ });
385
+ }
386
+ }, [value, formMethods]);
387
+ const currentValue = formMethods.watch("dateRange");
388
+ const dateRangesEqual = React3.useCallback((a, b) => {
389
+ if (a === b) {
390
+ return true;
391
+ }
392
+ if (!a || !b) {
393
+ return false;
394
+ }
395
+ const aFrom = a.from?.getTime();
396
+ const aTo = a.to?.getTime();
397
+ const bFrom = b.from?.getTime();
398
+ const bTo = b.to?.getTime();
399
+ return aFrom === bFrom && aTo === bTo;
400
+ }, []);
401
+ React3.useEffect(() => {
402
+ if (currentValue !== void 0 && !dateRangesEqual(currentValue, value)) {
403
+ onChange?.(currentValue);
404
+ }
405
+ }, [currentValue, value, onChange, dateRangesEqual]);
406
+ return /* @__PURE__ */React3.createElement(FormProvider, formMethods, /* @__PURE__ */React3.createElement(FormFieldDatePicker, {
407
+ name: "dateRange",
408
+ label,
409
+ presets
410
+ }));
411
+ }, "DateRangeFilter");
412
+
413
+ // src/Filters/SelectFilter.tsx
414
+ import { FormFieldSelect } from "@ttoss/forms";
415
+ import * as React4 from "react";
416
+ import { FormProvider as FormProvider2, useForm as useForm2 } from "react-hook-form";
417
+ var SelectFilter = /* @__PURE__ */__name(props => {
418
+ const {
419
+ value,
420
+ onChange,
421
+ label,
422
+ options
423
+ } = props;
424
+ const formMethods = useForm2({
425
+ defaultValues: {
426
+ value
427
+ },
428
+ mode: "onChange"
429
+ });
430
+ React4.useEffect(() => {
431
+ const propValue = value;
432
+ formMethods.setValue("value", propValue, {
433
+ shouldDirty: false
434
+ });
435
+ }, [value, formMethods]);
436
+ const currentValue = formMethods.watch("value");
437
+ React4.useEffect(() => {
438
+ if (currentValue !== void 0 && currentValue !== value) {
439
+ onChange(currentValue);
440
+ }
441
+ }, [currentValue, value, onChange]);
442
+ return /* @__PURE__ */React4.createElement(FormProvider2, formMethods, /* @__PURE__ */React4.createElement(FormFieldSelect, {
443
+ name: "value",
444
+ label,
445
+ options,
446
+ sx: {
447
+ fontSize: "sm"
448
+ }
449
+ }));
450
+ }, "SelectFilter");
451
+
452
+ // src/Filters/TextFilter.tsx
453
+ import { FormFieldInput } from "@ttoss/forms";
454
+ import * as React5 from "react";
455
+ import { FormProvider as FormProvider3, useForm as useForm3 } from "react-hook-form";
456
+ var TextFilter = /* @__PURE__ */__name(props => {
457
+ const {
458
+ value,
459
+ onChange,
460
+ label,
461
+ placeholder
462
+ } = props;
463
+ const formMethods = useForm3({
464
+ defaultValues: {
465
+ value: value.toString()
466
+ },
467
+ mode: "onChange"
468
+ });
469
+ React5.useEffect(() => {
470
+ const stringValue = value.toString();
471
+ formMethods.setValue("value", stringValue, {
472
+ shouldDirty: false
473
+ });
474
+ }, [value, formMethods]);
475
+ const currentValue = formMethods.watch("value");
476
+ React5.useEffect(() => {
477
+ if (currentValue !== void 0 && currentValue !== value.toString()) {
478
+ onChange(currentValue);
479
+ }
480
+ }, [currentValue, value, onChange]);
481
+ return /* @__PURE__ */React5.createElement(FormProvider3, formMethods, /* @__PURE__ */React5.createElement(FormFieldInput, {
482
+ name: "value",
483
+ label,
484
+ placeholder,
485
+ sx: {
486
+ fontSize: "sm"
487
+ }
488
+ }));
489
+ }, "TextFilter");
490
+
491
+ // src/DashboardFilters.tsx
492
+ var DashboardFilterType = /* @__PURE__ */function (DashboardFilterType2) {
493
+ DashboardFilterType2["TEXT"] = "text";
494
+ DashboardFilterType2["SELECT"] = "select";
495
+ DashboardFilterType2["DATE_RANGE"] = "date-range";
496
+ DashboardFilterType2["NUMBER"] = "number";
497
+ DashboardFilterType2["BOOLEAN"] = "boolean";
498
+ return DashboardFilterType2;
499
+ }({});
500
+ var DashboardFilters = /* @__PURE__ */__name(() => {
501
+ const {
502
+ filters,
503
+ updateFilter
504
+ } = useDashboard();
505
+ const onChangeHandlers = React6.useMemo(() => {
506
+ const handlers = /* @__PURE__ */new Map();
507
+ for (const filter of filters) {
508
+ handlers.set(filter.key, value => {
509
+ updateFilter(filter.key, value);
510
+ });
511
+ }
512
+ return handlers;
513
+ }, [filters, updateFilter]);
514
+ return /* @__PURE__ */React6.createElement(Flex4, {
515
+ sx: {
516
+ gap: "2",
517
+ flexDirection: "row",
518
+ "@media (max-width: 768px)": {
519
+ flexWrap: "wrap"
520
+ }
521
+ }
522
+ }, filters.map(filter => {
523
+ const onChange = onChangeHandlers.get(filter.key);
524
+ if (!onChange) {
525
+ return null;
526
+ }
527
+ switch (filter.type) {
528
+ case "text":
529
+ return /* @__PURE__ */React6.createElement(TextFilter, {
530
+ key: filter.key,
531
+ name: filter.key,
532
+ label: filter.label,
533
+ value: filter.value,
534
+ placeholder: filter.placeholder,
535
+ onChange
536
+ });
537
+ case "select":
538
+ return /* @__PURE__ */React6.createElement(SelectFilter, {
539
+ key: filter.key,
540
+ name: filter.key,
541
+ label: filter.label,
542
+ value: filter.value,
543
+ options: filter.options ?? [],
544
+ onChange: /* @__PURE__ */__name(value => {
545
+ onChange(value);
546
+ }, "onChange")
547
+ });
548
+ case "date-range":
549
+ return /* @__PURE__ */React6.createElement(DateRangeFilter, {
550
+ label: filter.label,
551
+ key: filter.key,
552
+ value: filter.value,
553
+ presets: filter.presets,
554
+ onChange: /* @__PURE__ */__name(range => {
555
+ onChange(range);
556
+ }, "onChange")
557
+ });
558
+ default:
559
+ return null;
560
+ }
561
+ }));
562
+ }, "DashboardFilters");
563
+
564
+ // src/DashboardHeader.tsx
565
+ var DashboardHeader = /* @__PURE__ */__name(({
566
+ children
567
+ }) => {
568
+ return /* @__PURE__ */React7.createElement(Flex5, {
569
+ sx: {
570
+ padding: "2"
571
+ }
572
+ }, /* @__PURE__ */React7.createElement(DashboardFilters, null), children);
573
+ }, "DashboardHeader");
574
+
575
+ // src/Dashboard.tsx
576
+ var DashboardContent = /* @__PURE__ */__name(({
577
+ loading = false,
578
+ headerChildren
579
+ }) => {
580
+ const {
581
+ selectedTemplate
582
+ } = useDashboard();
583
+ return /* @__PURE__ */React8.createElement(Flex6, {
584
+ sx: {
585
+ flexDirection: "column",
586
+ gap: "5",
587
+ padding: "2",
588
+ width: "100%"
589
+ }
590
+ }, /* @__PURE__ */React8.createElement(DashboardHeader, null, headerChildren), /* @__PURE__ */React8.createElement(Divider, null), /* @__PURE__ */React8.createElement(DashboardGrid, {
591
+ loading,
592
+ selectedTemplate
593
+ }));
594
+ }, "DashboardContent");
595
+ var Dashboard = /* @__PURE__ */__name(({
596
+ loading = false,
597
+ templates = [],
598
+ filters = [],
599
+ headerChildren,
600
+ onFiltersChange
601
+ }) => {
602
+ return /* @__PURE__ */React8.createElement(DashboardProvider, {
603
+ filters,
604
+ templates,
605
+ onFiltersChange
606
+ }, /* @__PURE__ */React8.createElement(DashboardContent, {
607
+ loading,
608
+ headerChildren
609
+ }));
610
+ }, "Dashboard");
611
+ export { Dashboard, DashboardCard, DashboardFilterType, DashboardFilters, DashboardGrid, DashboardHeader, DashboardProvider, useDashboard };
@@ -0,0 +1,116 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React from 'react';
3
+ import ReactGridLayout from 'react-grid-layout';
4
+ import { DateRange } from '@ttoss/components/DatePicker';
5
+
6
+ type CardNumberType = 'number' | 'percentage' | 'currency';
7
+ type CardSourceType = {
8
+ source: 'meta' | 'oneclickads' | 'api';
9
+ level?: 'adAccount' | 'campaign' | 'adSet' | 'ad';
10
+ };
11
+ type DashboardCardType = 'bigNumber' | 'pieChart' | 'barChart' | 'lineChart' | 'table' | 'list';
12
+ type DashboardCardData = {
13
+ meta?: {
14
+ total?: number;
15
+ daily?: number[];
16
+ };
17
+ api?: {
18
+ total?: number;
19
+ daily?: number[];
20
+ };
21
+ };
22
+ type CardVariant = 'default' | 'dark' | 'light-green';
23
+ type TrendIndicator = {
24
+ value: number;
25
+ status?: 'positive' | 'negative' | 'neutral';
26
+ };
27
+ type StatusIndicator = {
28
+ text: string;
29
+ icon?: string;
30
+ };
31
+ interface DashboardCard {
32
+ title: string;
33
+ description?: string;
34
+ icon?: string;
35
+ color?: string;
36
+ variant?: CardVariant;
37
+ numberType: CardNumberType;
38
+ type: DashboardCardType;
39
+ sourceType: CardSourceType[];
40
+ labels?: Array<string | number>;
41
+ data: DashboardCardData;
42
+ trend?: TrendIndicator;
43
+ additionalInfo?: string;
44
+ status?: StatusIndicator;
45
+ }
46
+ declare const DashboardCard: (props: DashboardCard) => react_jsx_runtime.JSX.Element | null;
47
+
48
+ type DashboardFilterValue = string | number | boolean | {
49
+ from: Date;
50
+ to: Date;
51
+ };
52
+ declare enum DashboardFilterType {
53
+ TEXT = "text",
54
+ SELECT = "select",
55
+ DATE_RANGE = "date-range",
56
+ NUMBER = "number",
57
+ BOOLEAN = "boolean"
58
+ }
59
+ type DashboardFilter = {
60
+ key: string;
61
+ type: DashboardFilterType;
62
+ label: string;
63
+ placeholder?: string;
64
+ value: DashboardFilterValue;
65
+ onChange?: (value: DashboardFilterValue) => void;
66
+ options?: {
67
+ label: string;
68
+ value: string | number | boolean;
69
+ }[];
70
+ presets?: {
71
+ label: string;
72
+ getValue: () => DateRange;
73
+ }[];
74
+ };
75
+ declare const DashboardFilters: () => react_jsx_runtime.JSX.Element;
76
+
77
+ type DashboardGridItem = ReactGridLayout.Layout & {
78
+ card: DashboardCard;
79
+ };
80
+ interface DashboardTemplate {
81
+ id: string;
82
+ name: string;
83
+ description?: string;
84
+ grid: DashboardGridItem[];
85
+ }
86
+ declare const Dashboard: ({ loading, templates, filters, headerChildren, onFiltersChange, }: {
87
+ loading?: boolean;
88
+ headerChildren?: React.ReactNode;
89
+ templates?: DashboardTemplate[];
90
+ filters?: DashboardFilter[];
91
+ onFiltersChange?: (filters: DashboardFilter[]) => void;
92
+ }) => react_jsx_runtime.JSX.Element;
93
+
94
+ declare const DashboardGrid: ({ loading, selectedTemplate, }: {
95
+ loading: boolean;
96
+ selectedTemplate?: DashboardTemplate;
97
+ }) => react_jsx_runtime.JSX.Element | null;
98
+
99
+ declare const DashboardHeader: ({ children, }: {
100
+ children?: React.ReactNode;
101
+ }) => react_jsx_runtime.JSX.Element;
102
+
103
+ declare const DashboardProvider: (props: {
104
+ children: React.ReactNode;
105
+ filters: DashboardFilter[];
106
+ templates: DashboardTemplate[];
107
+ onFiltersChange?: (filters: DashboardFilter[]) => void;
108
+ }) => react_jsx_runtime.JSX.Element;
109
+ declare const useDashboard: () => {
110
+ filters: DashboardFilter[];
111
+ updateFilter: (key: string, value: DashboardFilterValue) => void;
112
+ templates: DashboardTemplate[];
113
+ selectedTemplate: DashboardTemplate | undefined;
114
+ };
115
+
116
+ export { Dashboard, DashboardCard, type DashboardFilter, DashboardFilterType, type DashboardFilterValue, DashboardFilters, DashboardGrid, type DashboardGridItem, DashboardHeader, DashboardProvider, type DashboardTemplate, useDashboard };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ttoss/react-dashboard",
3
+ "version": "0.1.1",
4
+ "description": "ttoss dashboard module for React apps.",
5
+ "license": "MIT",
6
+ "author": "ttoss",
7
+ "contributors": [
8
+ "Pedro Arantes <pedro@arantespp.com> (https://arantespp.com/contact)"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/ttoss/ttoss.git",
13
+ "directory": "packages/react-dashboard"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/esm/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "peerDependencies": {
27
+ "react": ">=16.8.0",
28
+ "react-day-picker": "^9.11.2",
29
+ "react-grid-layout": "^1.5.2",
30
+ "react-hook-form": "^7.66.1",
31
+ "@ttoss/components": "^2.10.1",
32
+ "@ttoss/forms": "^0.35.2",
33
+ "@ttoss/config": "^1.35.12",
34
+ "@ttoss/test-utils": "^4.0.1",
35
+ "@ttoss/react-icons": "^0.5.5",
36
+ "@ttoss/ui": "^6.0.4"
37
+ },
38
+ "devDependencies": {
39
+ "@types/react": "^19.2.6",
40
+ "@types/react-grid-layout": "^1.3.6",
41
+ "jest": "^30.2.0",
42
+ "react-hook-form": "^7.66.1",
43
+ "tsup": "^8.5.1",
44
+ "@ttoss/components": "^2.10.1",
45
+ "@ttoss/forms": "^0.35.2"
46
+ },
47
+ "keywords": [
48
+ "React",
49
+ "dashboard"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "provenance": true
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "test": "jest --projects tests/unit"
58
+ }
59
+ }