@wger-project/react-components 25.11.22 → 25.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/assets/index.css +1 -1
- package/build/locales/de/translation.json +13 -4
- package/build/locales/en/translation.json +9 -0
- package/build/locales/hi/translation.json +5 -0
- package/build/locales/nl/translation.json +242 -238
- package/build/locales/pt_BR/translation.json +340 -255
- package/build/main.js +156 -152
- package/build/main.js.map +1 -1
- package/package.json +5 -2
- package/src/components/Dashboard/ConfigurableDashboard.tsx +228 -0
- package/src/components/Dashboard/DashboardCard.tsx +121 -0
- package/src/components/Dashboard/EmptyCard.tsx +3 -3
- package/src/components/Dashboard/GoalCard.tsx +71 -0
- package/src/components/Dashboard/NutritionCard.tsx +88 -96
- package/src/components/Dashboard/RoutineCard.tsx +54 -69
- package/src/components/Dashboard/WeightCard.tsx +36 -42
- package/src/components/Exercises/Detail/Head/ExerciseDeleteDialog.tsx +1 -1
- package/src/components/WorkoutRoutines/Detail/WorkoutStats.tsx +1 -1
- package/src/components/WorkoutRoutines/widgets/forms/DayTypeSelect.tsx +1 -2
- package/src/components/WorkoutRoutines/widgets/forms/SlotForm.tsx +0 -4
- package/src/index.tsx +0 -46
- package/src/routes.tsx +82 -79
- package/src/services/exerciseTranslation.ts +5 -6
- package/src/services/video.test.ts +4 -4
- package/src/components/Dashboard/Dashboard.tsx +0 -22
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./build/index.js"
|
|
13
13
|
},
|
|
14
|
-
"version": "25.
|
|
14
|
+
"version": "25.12.5",
|
|
15
15
|
"repository": "https://github.com/wger-project/react",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"publishConfig": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"luxon": "^3.7.2",
|
|
40
40
|
"react": "^19.2.0",
|
|
41
41
|
"react-dom": "^19.2.0",
|
|
42
|
+
"react-grid-layout": "^1.5.2",
|
|
42
43
|
"react-i18next": "^16.3.3",
|
|
43
44
|
"react-is": "^19.2.0",
|
|
44
45
|
"react-responsive": "^10.0.1",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"@types/node": "^22.18.9",
|
|
65
66
|
"@types/react": "^19.2.5",
|
|
66
67
|
"@types/react-dom": "^19.2.3",
|
|
68
|
+
"@types/react-grid-layout": "^1.3.5",
|
|
67
69
|
"@types/react-is": "^19.2.0",
|
|
68
70
|
"@types/slug": "^5.0.9",
|
|
69
71
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
|
@@ -81,7 +83,8 @@
|
|
|
81
83
|
"typescript-eslint": "^8.46.0",
|
|
82
84
|
"vite": "^7.2.2",
|
|
83
85
|
"vite-plugin-eslint": "^1.8.1",
|
|
84
|
-
"vitest": "^3.2.4"
|
|
86
|
+
"vitest": "^3.2.4",
|
|
87
|
+
"webpack-bundle-analyzer": "^4.10.2"
|
|
85
88
|
},
|
|
86
89
|
"scripts": {
|
|
87
90
|
"start": "vite",
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Box, Button, IconButton, Tooltip } from "@mui/material";
|
|
2
|
+
import DashboardCustomizeIcon from "@mui/icons-material/DashboardCustomize";
|
|
3
|
+
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
4
|
+
import DoneIcon from '@mui/icons-material/Done';
|
|
5
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
6
|
+
import { Responsive, WidthProvider, Layout, Layouts } from "react-grid-layout";
|
|
7
|
+
import "react-grid-layout/css/styles.css";
|
|
8
|
+
import "react-resizable/css/styles.css";
|
|
9
|
+
import { NutritionCard } from "components/Dashboard/NutritionCard";
|
|
10
|
+
import { RoutineCard } from "components/Dashboard/RoutineCard";
|
|
11
|
+
import { WeightCard } from "components/Dashboard/WeightCard";
|
|
12
|
+
import { useTranslation } from "react-i18next";
|
|
13
|
+
|
|
14
|
+
const ResponsiveGridLayout = WidthProvider(Responsive);
|
|
15
|
+
|
|
16
|
+
// Local storage key for persisting layouts
|
|
17
|
+
const LAYOUT_STORAGE_KEY = "dashboard-layout";
|
|
18
|
+
|
|
19
|
+
// Define widget types for extensibility
|
|
20
|
+
export type WidgetType = "routine" | "nutrition" | "weight";
|
|
21
|
+
|
|
22
|
+
export interface WidgetConfig {
|
|
23
|
+
id: string;
|
|
24
|
+
type: WidgetType;
|
|
25
|
+
component: React.ComponentType;
|
|
26
|
+
defaultLayout: {
|
|
27
|
+
w: number;
|
|
28
|
+
h: number;
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
minW?: number;
|
|
32
|
+
minH?: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Widget registry - easy to add new widgets in the future
|
|
37
|
+
export const AVAILABLE_WIDGETS: WidgetConfig[] = [
|
|
38
|
+
{
|
|
39
|
+
id: "routine",
|
|
40
|
+
type: "routine",
|
|
41
|
+
component: RoutineCard,
|
|
42
|
+
defaultLayout: { w: 4, h: 5, x: 0, y: 0, minW: 3, minH: 2 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "nutrition",
|
|
46
|
+
type: "nutrition",
|
|
47
|
+
component: NutritionCard,
|
|
48
|
+
defaultLayout: { w: 4, h: 5, x: 4, y: 0, minW: 3, minH: 2 },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "weight",
|
|
52
|
+
type: "weight",
|
|
53
|
+
component: WeightCard,
|
|
54
|
+
defaultLayout: { w: 4, h: 5, x: 8, y: 0, minW: 3, minH: 2 },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Generate default layouts for all breakpoints
|
|
59
|
+
const generateDefaultLayouts = (): Layouts => {
|
|
60
|
+
const lg: Layout[] = AVAILABLE_WIDGETS.map((widget) => ({
|
|
61
|
+
i: widget.id,
|
|
62
|
+
...widget.defaultLayout,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// For medium screens, make widgets full width in pairs
|
|
66
|
+
const md: Layout[] = AVAILABLE_WIDGETS.map((widget, index) => ({
|
|
67
|
+
i: widget.id,
|
|
68
|
+
w: 6,
|
|
69
|
+
h: widget.defaultLayout.h,
|
|
70
|
+
x: (index % 2) * 6,
|
|
71
|
+
y: Math.floor(index / 2) * widget.defaultLayout.h,
|
|
72
|
+
minW: widget.defaultLayout.minW,
|
|
73
|
+
minH: widget.defaultLayout.minH,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// For small screens, stack vertically
|
|
77
|
+
const sm: Layout[] = AVAILABLE_WIDGETS.map((widget, index) => ({
|
|
78
|
+
i: widget.id,
|
|
79
|
+
w: 12,
|
|
80
|
+
h: widget.defaultLayout.h,
|
|
81
|
+
x: 0,
|
|
82
|
+
y: index * widget.defaultLayout.h,
|
|
83
|
+
minW: widget.defaultLayout.minW,
|
|
84
|
+
minH: widget.defaultLayout.minH,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return { lg, md, sm, xs: sm };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Load layouts from localStorage
|
|
91
|
+
const loadLayouts = (): Layouts | null => {
|
|
92
|
+
try {
|
|
93
|
+
const saved = localStorage.getItem(LAYOUT_STORAGE_KEY);
|
|
94
|
+
return saved ? JSON.parse(saved) : null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("Error loading dashboard layout:", error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Save layouts to localStorage
|
|
102
|
+
const saveLayouts = (layouts: Layouts) => {
|
|
103
|
+
try {
|
|
104
|
+
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(layouts));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("Error saving dashboard layout:", error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const ConfigurableDashboard: React.FC = () => {
|
|
111
|
+
const [t] = useTranslation();
|
|
112
|
+
const [isEditMode, setIsEditMode] = useState(false);
|
|
113
|
+
const [layouts, setLayouts] = useState<Layouts>(() => {
|
|
114
|
+
const savedLayouts = loadLayouts();
|
|
115
|
+
return savedLayouts || generateDefaultLayouts();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const handleLayoutChange = useCallback((_: Layout[], allLayouts: Layouts) => {
|
|
119
|
+
setLayouts(allLayouts);
|
|
120
|
+
saveLayouts(allLayouts);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const handleResetLayout = useCallback(() => {
|
|
124
|
+
const defaultLayouts = generateDefaultLayouts();
|
|
125
|
+
setLayouts(defaultLayouts);
|
|
126
|
+
saveLayouts(defaultLayouts);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const toggleEditMode = useCallback(() => {
|
|
130
|
+
setIsEditMode((prev) => !prev);
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
// Grid configuration
|
|
134
|
+
const gridConfig = useMemo(
|
|
135
|
+
() => ({
|
|
136
|
+
className: "layout",
|
|
137
|
+
layouts: layouts,
|
|
138
|
+
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480 },
|
|
139
|
+
cols: { lg: 12, md: 12, sm: 12, xs: 12 },
|
|
140
|
+
rowHeight: 100,
|
|
141
|
+
isDraggable: isEditMode,
|
|
142
|
+
isResizable: isEditMode,
|
|
143
|
+
onLayoutChange: handleLayoutChange,
|
|
144
|
+
draggableHandle: isEditMode ? undefined : ".no-drag",
|
|
145
|
+
margin: [16, 16] as [number, number],
|
|
146
|
+
containerPadding: [0, 0] as [number, number],
|
|
147
|
+
}),
|
|
148
|
+
[layouts, isEditMode, handleLayoutChange]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Box>
|
|
153
|
+
<Box
|
|
154
|
+
sx={{
|
|
155
|
+
display: "flex",
|
|
156
|
+
justifyContent: "flex-end",
|
|
157
|
+
my: 1,
|
|
158
|
+
mx: 2,
|
|
159
|
+
gap: 1,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{isEditMode && (
|
|
163
|
+
<Box
|
|
164
|
+
sx={{
|
|
165
|
+
p: 1,
|
|
166
|
+
borderRadius: 1,
|
|
167
|
+
fontSize: "0.875rem",
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{t('dashboard.dragWidgetsHelp')}
|
|
171
|
+
</Box>
|
|
172
|
+
)}
|
|
173
|
+
{isEditMode && (
|
|
174
|
+
<Tooltip title={t('dashboard.resetLayout')}>
|
|
175
|
+
<IconButton
|
|
176
|
+
onClick={handleResetLayout}
|
|
177
|
+
size="small"
|
|
178
|
+
sx={{
|
|
179
|
+
color: "primary.main",
|
|
180
|
+
borderRadius: 1,
|
|
181
|
+
fontSize: "0.875rem",
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<RestartAltIcon />
|
|
185
|
+
</IconButton>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
)}
|
|
188
|
+
<Tooltip title={isEditMode ? t('core.exitEditMode') : t('dashboard.customizeDashboard')}>
|
|
189
|
+
<Button
|
|
190
|
+
variant={isEditMode ? "contained" : "outlined"}
|
|
191
|
+
startIcon={isEditMode ? <DoneIcon /> : <DashboardCustomizeIcon />}
|
|
192
|
+
onClick={toggleEditMode}
|
|
193
|
+
size="small"
|
|
194
|
+
sx={{ p: 1 }}
|
|
195
|
+
>
|
|
196
|
+
{isEditMode ? t('save') : t('core.customize')}
|
|
197
|
+
</Button>
|
|
198
|
+
</Tooltip>
|
|
199
|
+
</Box>
|
|
200
|
+
|
|
201
|
+
<ResponsiveGridLayout {...gridConfig}>
|
|
202
|
+
{AVAILABLE_WIDGETS.map((widget) => {
|
|
203
|
+
const WidgetComponent = widget.component;
|
|
204
|
+
return (
|
|
205
|
+
<Box
|
|
206
|
+
key={widget.id}
|
|
207
|
+
sx={{
|
|
208
|
+
// Add visual feedback in edit mode
|
|
209
|
+
border: isEditMode ? "1px dashed" : "none",
|
|
210
|
+
borderColor: "primary.main",
|
|
211
|
+
borderRadius: 1,
|
|
212
|
+
transition: "border 0.2s",
|
|
213
|
+
cursor: isEditMode ? "move" : "default",
|
|
214
|
+
"&:hover": isEditMode
|
|
215
|
+
? {
|
|
216
|
+
borderColor: "primary.dark",
|
|
217
|
+
}
|
|
218
|
+
: {},
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<WidgetComponent />
|
|
222
|
+
</Box>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</ResponsiveGridLayout>
|
|
226
|
+
</Box>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Card, CardActions, CardContent, CardHeader } from "@mui/material";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
export interface DashboardCardProps {
|
|
5
|
+
/**
|
|
6
|
+
* Card title displayed in the header
|
|
7
|
+
*/
|
|
8
|
+
title: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Optional subtitle displayed below the title
|
|
12
|
+
*/
|
|
13
|
+
subheader?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main content of the card
|
|
17
|
+
*/
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional actions to display at the bottom of the card (buttons, icons, etc.)
|
|
22
|
+
*/
|
|
23
|
+
actions?: React.ReactNode;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional header action (typically an icon button in the top-right)
|
|
27
|
+
*/
|
|
28
|
+
headerAction?: React.ReactNode;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Custom height for the content area (default: auto-fills available space)
|
|
32
|
+
* Use this if you need to override the default flex behavior
|
|
33
|
+
*/
|
|
34
|
+
contentHeight?: string | number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the content should scroll when it overflows (default: true)
|
|
38
|
+
*/
|
|
39
|
+
scrollable?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Additional sx props for the Card component
|
|
43
|
+
*/
|
|
44
|
+
cardSx?: React.ComponentProps<typeof Card>["sx"];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Additional sx props for the CardContent component
|
|
48
|
+
*/
|
|
49
|
+
contentSx?: React.ComponentProps<typeof CardContent>["sx"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* DashboardCard - A reusable card component for the configurable dashboard
|
|
54
|
+
*
|
|
55
|
+
* This component handles all the responsive layout logic so individual widgets
|
|
56
|
+
* don't need to worry about flexbox, height calculations, or scrolling.
|
|
57
|
+
*
|
|
58
|
+
* Features:
|
|
59
|
+
* - Automatically fills the grid cell height
|
|
60
|
+
* - Proper scrolling behavior
|
|
61
|
+
* - Consistent styling across all dashboard widgets
|
|
62
|
+
* - Easy to use for future widgets
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <DashboardCard
|
|
67
|
+
* title="My Widget"
|
|
68
|
+
* subheader="Widget description"
|
|
69
|
+
* actions={<Button>See Details</Button>}
|
|
70
|
+
* >
|
|
71
|
+
* <YourContent />
|
|
72
|
+
* </DashboardCard>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const DashboardCard: React.FC<DashboardCardProps> = ({
|
|
76
|
+
title,
|
|
77
|
+
subheader,
|
|
78
|
+
children,
|
|
79
|
+
actions,
|
|
80
|
+
headerAction,
|
|
81
|
+
contentHeight,
|
|
82
|
+
scrollable = true,
|
|
83
|
+
cardSx = {},
|
|
84
|
+
contentSx = {},
|
|
85
|
+
}) => {
|
|
86
|
+
return (
|
|
87
|
+
<Card
|
|
88
|
+
sx={{
|
|
89
|
+
height: "100%",
|
|
90
|
+
display: "flex",
|
|
91
|
+
flexDirection: "column",
|
|
92
|
+
...cardSx,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<CardHeader title={title} subheader={subheader} action={headerAction} />
|
|
96
|
+
|
|
97
|
+
<CardContent
|
|
98
|
+
sx={{
|
|
99
|
+
flexGrow: 1,
|
|
100
|
+
overflow: scrollable ? "auto" : "visible",
|
|
101
|
+
minHeight: 0, // Critical for flexbox scrolling
|
|
102
|
+
height: contentHeight,
|
|
103
|
+
...contentSx,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</CardContent>
|
|
108
|
+
|
|
109
|
+
{actions && (
|
|
110
|
+
<CardActions
|
|
111
|
+
sx={{
|
|
112
|
+
justifyContent: "space-between",
|
|
113
|
+
alignItems: "flex-start",
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{actions}
|
|
117
|
+
</CardActions>
|
|
118
|
+
)}
|
|
119
|
+
</Card>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -32,14 +32,14 @@ export const EmptyCard = (props: {
|
|
|
32
32
|
</Button>;
|
|
33
33
|
|
|
34
34
|
return (<>
|
|
35
|
-
<Card>
|
|
35
|
+
<Card sx={{ paddingTop: 0, height: "100%", }}>
|
|
36
36
|
<CardHeader
|
|
37
37
|
title={props.title}
|
|
38
38
|
subheader={'.'}
|
|
39
39
|
sx={{ paddingBottom: 0 }} />
|
|
40
40
|
|
|
41
|
-
<CardContent
|
|
42
|
-
<OverviewEmpty />
|
|
41
|
+
<CardContent>
|
|
42
|
+
<OverviewEmpty height={'50%'} />
|
|
43
43
|
</CardContent>
|
|
44
44
|
|
|
45
45
|
<CardActions>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import TrendingUpIcon from "@mui/icons-material/TrendingUp";
|
|
2
|
+
import { Button, IconButton, List, ListItem, ListItemText, Typography } from "@mui/material";
|
|
3
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
4
|
+
import { DashboardCard } from "components/Dashboard/DashboardCard";
|
|
5
|
+
import React from "react";
|
|
6
|
+
// import { useTranslation } from "react-i18next";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Example of a new widget using DashboardCard
|
|
10
|
+
*
|
|
11
|
+
* This demonstrates how simple it is to create a new dashboard widget.
|
|
12
|
+
* All the responsive layout logic is handled by DashboardCard!
|
|
13
|
+
*/
|
|
14
|
+
export const GoalsCard = () => {
|
|
15
|
+
// const { t } = useTranslation();
|
|
16
|
+
|
|
17
|
+
// Your data fetching logic here
|
|
18
|
+
const goals = [
|
|
19
|
+
{ id: 1, title: "Lose 5kg", progress: 60 },
|
|
20
|
+
{ id: 2, title: "Run 5km", progress: 80 },
|
|
21
|
+
{ id: 3, title: "Bench 100kg", progress: 45 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<DashboardCard
|
|
26
|
+
title="My Goals"
|
|
27
|
+
subheader="Track your fitness goals"
|
|
28
|
+
// Optional header action (top-right icon)
|
|
29
|
+
headerAction={
|
|
30
|
+
<Tooltip title="View trends">
|
|
31
|
+
<IconButton>
|
|
32
|
+
<TrendingUpIcon />
|
|
33
|
+
</IconButton>
|
|
34
|
+
</Tooltip>
|
|
35
|
+
}
|
|
36
|
+
// Actions at the bottom of the card
|
|
37
|
+
actions={
|
|
38
|
+
<>
|
|
39
|
+
<Button size="small">See All Goals</Button>
|
|
40
|
+
<Button size="small" variant="contained">
|
|
41
|
+
Add Goal
|
|
42
|
+
</Button>
|
|
43
|
+
</>
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
{/* Your card content goes here */}
|
|
47
|
+
<List>
|
|
48
|
+
{goals.map((goal) => (
|
|
49
|
+
<ListItem key={goal.id}>
|
|
50
|
+
<ListItemText
|
|
51
|
+
primary={goal.title}
|
|
52
|
+
secondary={
|
|
53
|
+
<Typography variant="body2" color="text.secondary">
|
|
54
|
+
Progress: {goal.progress}%
|
|
55
|
+
</Typography>
|
|
56
|
+
}
|
|
57
|
+
/>
|
|
58
|
+
</ListItem>
|
|
59
|
+
))}
|
|
60
|
+
</List>
|
|
61
|
+
</DashboardCard>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// To add this widget to the dashboard, just add it to AVAILABLE_WIDGETS in ConfigurableDashboard.tsx:
|
|
66
|
+
// {
|
|
67
|
+
// id: 'goals',
|
|
68
|
+
// type: 'goals',
|
|
69
|
+
// component: GoalsCard,
|
|
70
|
+
// defaultLayout: { w: 4, h: 6, x: 0, y: 6, minW: 3, minH: 4 },
|
|
71
|
+
// }
|