@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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "exports": {
12
12
  ".": "./build/index.js"
13
13
  },
14
- "version": "25.11.22",
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 sx={{ paddingTop: 0, height: "500px", }}>
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
+ // }