mobigrid-module 1.0.7 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ import * as React from "react"
2
+ import * as SelectPrimitive from "@radix-ui/react-select"
3
+ import { Check, ChevronDown, ChevronUp } from "lucide-react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Select = SelectPrimitive.Root
8
+
9
+ const SelectGroup = SelectPrimitive.Group
10
+
11
+ const SelectValue = SelectPrimitive.Value
12
+
13
+ const SelectTrigger = React.forwardRef<
14
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
15
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
+ >(({ className, children, ...props }, ref) => (
17
+ <SelectPrimitive.Trigger
18
+ ref={ref}
19
+ className={cn(
20
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon asChild>
27
+ <ChevronDown className="h-4 w-4 opacity-50" />
28
+ </SelectPrimitive.Icon>
29
+ </SelectPrimitive.Trigger>
30
+ ))
31
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32
+
33
+ const SelectScrollUpButton = React.forwardRef<
34
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
35
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
36
+ >(({ className, ...props }, ref) => (
37
+ <SelectPrimitive.ScrollUpButton
38
+ ref={ref}
39
+ className={cn(
40
+ "flex cursor-default items-center justify-center py-1",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ <ChevronUp className="h-4 w-4" />
46
+ </SelectPrimitive.ScrollUpButton>
47
+ ))
48
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
49
+
50
+ const SelectScrollDownButton = React.forwardRef<
51
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
52
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
53
+ >(({ className, ...props }, ref) => (
54
+ <SelectPrimitive.ScrollDownButton
55
+ ref={ref}
56
+ className={cn(
57
+ "flex cursor-default items-center justify-center py-1",
58
+ className
59
+ )}
60
+ {...props}
61
+ >
62
+ <ChevronDown className="h-4 w-4" />
63
+ </SelectPrimitive.ScrollDownButton>
64
+ ))
65
+ SelectScrollDownButton.displayName =
66
+ SelectPrimitive.ScrollDownButton.displayName
67
+
68
+ const SelectContent = React.forwardRef<
69
+ React.ElementRef<typeof SelectPrimitive.Content>,
70
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
71
+ >(({ className, children, position = "popper", ...props }, ref) => (
72
+ <SelectPrimitive.Portal>
73
+ <SelectPrimitive.Content
74
+ ref={ref}
75
+ className={cn(
76
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
77
+ position === "popper" &&
78
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
79
+ className
80
+ )}
81
+ position={position}
82
+ {...props}
83
+ >
84
+ <SelectScrollUpButton />
85
+ <SelectPrimitive.Viewport
86
+ className={cn(
87
+ "p-1",
88
+ position === "popper" &&
89
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
90
+ )}
91
+ >
92
+ {children}
93
+ </SelectPrimitive.Viewport>
94
+ <SelectScrollDownButton />
95
+ </SelectPrimitive.Content>
96
+ </SelectPrimitive.Portal>
97
+ ))
98
+ SelectContent.displayName = SelectPrimitive.Content.displayName
99
+
100
+ const SelectLabel = React.forwardRef<
101
+ React.ElementRef<typeof SelectPrimitive.Label>,
102
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
103
+ >(({ className, ...props }, ref) => (
104
+ <SelectPrimitive.Label
105
+ ref={ref}
106
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
107
+ {...props}
108
+ />
109
+ ))
110
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
111
+
112
+ const SelectItem = React.forwardRef<
113
+ React.ElementRef<typeof SelectPrimitive.Item>,
114
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
115
+ >(({ className, children, ...props }, ref) => (
116
+ <SelectPrimitive.Item
117
+ ref={ref}
118
+ className={cn(
119
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
120
+ className
121
+ )}
122
+ {...props}
123
+ >
124
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
125
+ <SelectPrimitive.ItemIndicator>
126
+ <Check className="h-4 w-4" />
127
+ </SelectPrimitive.ItemIndicator>
128
+ </span>
129
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
130
+ </SelectPrimitive.Item>
131
+ ))
132
+ SelectItem.displayName = SelectPrimitive.Item.displayName
133
+
134
+ const SelectSeparator = React.forwardRef<
135
+ React.ElementRef<typeof SelectPrimitive.Separator>,
136
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
137
+ >(({ className, ...props }, ref) => (
138
+ <SelectPrimitive.Separator
139
+ ref={ref}
140
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
141
+ {...props}
142
+ />
143
+ ))
144
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
145
+
146
+ export {
147
+ Select,
148
+ SelectGroup,
149
+ SelectValue,
150
+ SelectTrigger,
151
+ SelectContent,
152
+ SelectLabel,
153
+ SelectItem,
154
+ SelectSeparator,
155
+ SelectScrollUpButton,
156
+ SelectScrollDownButton,
157
+ }
@@ -0,0 +1,30 @@
1
+ import React from "react"
2
+ import { useTheme } from "next-themes"
3
+ import { Toaster as Sonner } from "sonner"
4
+
5
+ type ToasterProps = React.ComponentProps<typeof Sonner>
6
+
7
+ const Toaster = ({ ...props }: ToasterProps) => {
8
+ const { theme = "system" } = useTheme()
9
+
10
+ return (
11
+ <Sonner
12
+ theme={theme as ToasterProps["theme"]}
13
+ className="toaster group"
14
+ toastOptions={{
15
+ classNames: {
16
+ toast:
17
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
18
+ description: "group-[.toast]:text-muted-foreground",
19
+ actionButton:
20
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
21
+ cancelButton:
22
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
23
+ },
24
+ }}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ export { Toaster }
@@ -0,0 +1,120 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.HTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div className="relative w-full overflow-auto">
10
+ <table
11
+ ref={ref}
12
+ className={cn("w-full caption-bottom text-sm", className)}
13
+ {...props}
14
+ />
15
+ </div>
16
+ ))
17
+ Table.displayName = "Table"
18
+
19
+ const TableHeader = React.forwardRef<
20
+ HTMLTableSectionElement,
21
+ React.HTMLAttributes<HTMLTableSectionElement>
22
+ >(({ className, ...props }, ref) => (
23
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
24
+ ))
25
+ TableHeader.displayName = "TableHeader"
26
+
27
+ const TableBody = React.forwardRef<
28
+ HTMLTableSectionElement,
29
+ React.HTMLAttributes<HTMLTableSectionElement>
30
+ >(({ className, ...props }, ref) => (
31
+ <tbody
32
+ ref={ref}
33
+ className={cn("[&_tr:last-child]:border-0", className)}
34
+ {...props}
35
+ />
36
+ ))
37
+ TableBody.displayName = "TableBody"
38
+
39
+ const TableFooter = React.forwardRef<
40
+ HTMLTableSectionElement,
41
+ React.HTMLAttributes<HTMLTableSectionElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <tfoot
44
+ ref={ref}
45
+ className={cn(
46
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ))
52
+ TableFooter.displayName = "TableFooter"
53
+
54
+ const TableRow = React.forwardRef<
55
+ HTMLTableRowElement,
56
+ React.HTMLAttributes<HTMLTableRowElement>
57
+ >(({ className, ...props }, ref) => (
58
+ <tr
59
+ ref={ref}
60
+ className={cn(
61
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ))
67
+ TableRow.displayName = "TableRow"
68
+
69
+ const TableHead = React.forwardRef<
70
+ HTMLTableCellElement,
71
+ React.ThHTMLAttributes<HTMLTableCellElement>
72
+ >(({ className, ...props }, ref) => (
73
+ <th
74
+ ref={ref}
75
+ className={cn(
76
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ ))
82
+ TableHead.displayName = "TableHead"
83
+
84
+ const TableCell = React.forwardRef<
85
+ HTMLTableCellElement,
86
+ React.TdHTMLAttributes<HTMLTableCellElement>
87
+ >(({ className, ...props }, ref) => (
88
+ <td
89
+ ref={ref}
90
+ className={cn(
91
+ "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ TableCell.displayName = "TableCell"
98
+
99
+ const TableCaption = React.forwardRef<
100
+ HTMLTableCaptionElement,
101
+ React.HTMLAttributes<HTMLTableCaptionElement>
102
+ >(({ className, ...props }, ref) => (
103
+ <caption
104
+ ref={ref}
105
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ TableCaption.displayName = "TableCaption"
110
+
111
+ export {
112
+ Table,
113
+ TableHeader,
114
+ TableBody,
115
+ TableFooter,
116
+ TableHead,
117
+ TableRow,
118
+ TableCell,
119
+ TableCaption,
120
+ }
package/index.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ declare module "mobigrid-module" {
2
+ import { ReactNode } from "react";
3
+
4
+ export interface FilterOption {
5
+ name: string;
6
+ type: string;
7
+ urlSource?: string;
8
+ options?: any[];
9
+ label?: string;
10
+ placeholder?: string;
11
+ required?: boolean;
12
+ }
13
+
14
+ export interface Column {
15
+ field: string;
16
+ headerName: string;
17
+ width?: number;
18
+ sortable?: boolean;
19
+ renderCell?: (params: any) => ReactNode;
20
+ }
21
+
22
+ export interface Config {
23
+ title: string;
24
+ data_url: string;
25
+ colomns: Column[];
26
+ Filters?: FilterOption[][];
27
+ }
28
+
29
+ export interface FilterState {
30
+ fromDate: string;
31
+ toDate: string;
32
+ [key: string]: any;
33
+ }
34
+
35
+ export interface ApiResponse {
36
+ DATA: any[];
37
+ PAGESNUM: number;
38
+ }
39
+
40
+ export interface Error {
41
+ title: string;
42
+ message: string;
43
+ }
44
+
45
+ export interface MobigridModuleProps {
46
+ configUrl: string;
47
+ preJsUrl: string;
48
+ itemsPerPage?: number;
49
+ children?: ReactNode;
50
+ }
51
+
52
+ const MobigridModule: React.FC<MobigridModuleProps>;
53
+
54
+ export default MobigridModule;
55
+ }
package/index.tsx ADDED
@@ -0,0 +1,254 @@
1
+ import React from "react";
2
+ import { useEffect, useState } from "react";
3
+ import { format } from "date-fns";
4
+ import axios from "axios";
5
+ import { Alert, AlertTitle, AlertDescription } from "./components/ui/alert";
6
+ import { AlertCircle } from "lucide-react";
7
+ import { PageHeader } from "./components/Layout/PageHeader";
8
+ import { CustomTable } from "./components/CustomTable/CustomTable";
9
+ import Pagination from "./components/CustomTable/Pagination";
10
+
11
+ const ITEMS_PER_PAGE = 14;
12
+
13
+ interface ContainerProps {
14
+ configUrl: string;
15
+ preJsUrl: string;
16
+ itemsPerPage?: number;
17
+ children?: React.ReactNode;
18
+ }
19
+
20
+ const MobigridModule = ({
21
+ configUrl,
22
+ preJsUrl,
23
+ itemsPerPage = ITEMS_PER_PAGE,
24
+ children,
25
+ }: ContainerProps) => {
26
+ const [config, setConfig] = useState<any>(null);
27
+ const [invalidConfig, setInvalidConfig] = useState(false);
28
+ const [errors, setErrors] = useState<
29
+ Array<{
30
+ title: string;
31
+ message: string;
32
+ }>
33
+ >([]);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [data, setData] = useState([]);
36
+ const [count, setCount] = useState(0);
37
+ const [currentPage, setCurrentPage] = useState(1);
38
+ const [totalPages, setTotalPages] = useState(1);
39
+ const [filters, setFilters] = useState({
40
+ fromDate: new Date(
41
+ new Date().getFullYear(),
42
+ new Date().getMonth(),
43
+ 1
44
+ ).toLocaleString(),
45
+ toDate: new Date(
46
+ new Date().getFullYear(),
47
+ new Date().getMonth() + 1,
48
+ 0,
49
+ 23,
50
+ 59,
51
+ 59
52
+ ).toLocaleString(),
53
+ });
54
+ const [filterOptions, setFilterOptions] = useState<any[]>([]);
55
+
56
+ useEffect(() => {
57
+ const loadConfig = async () => {
58
+ try {
59
+ const response = await fetch(configUrl);
60
+ const data = await response.json();
61
+ if (data && data.title && data.data_url && data.colomns) {
62
+ setConfig(data);
63
+ setFilterOptions(data.Filters?.flat() || []);
64
+ } else {
65
+ setInvalidConfig(true);
66
+ }
67
+ } catch (error) {
68
+ setInvalidConfig(true);
69
+ }
70
+ };
71
+
72
+ const loadFunctions = async () => {
73
+ const response = await fetch(preJsUrl);
74
+ const script = document.createElement("script");
75
+ script.textContent = await response.text();
76
+ document.head.appendChild(script);
77
+ };
78
+
79
+ if (configUrl && preJsUrl) {
80
+ loadConfig();
81
+ loadFunctions();
82
+ }
83
+ }, [configUrl, preJsUrl]);
84
+
85
+ useEffect(() => {
86
+ const controller = new AbortController();
87
+
88
+ const initializeFilters = async () => {
89
+ const filters = config?.Filters?.flat() || [];
90
+ for (const filter of filters) {
91
+ if (filter.type === "Select" && filter.urlSource) {
92
+ await fetchFilterOptions(filter, controller.signal);
93
+ }
94
+ }
95
+ };
96
+
97
+ if (config) initializeFilters();
98
+
99
+ return () => controller.abort();
100
+ }, [config]);
101
+
102
+ const getData = async () => {
103
+ setErrors([]);
104
+ const postBody = {
105
+ page: currentPage,
106
+ filters: {
107
+ ...filters,
108
+ fromDate: format(filters.fromDate, "MM-dd-yyyy HH:mm"),
109
+ toDate: format(filters.toDate, "MM-dd-yyyy HH:mm"),
110
+ },
111
+ };
112
+
113
+ setIsLoading(true);
114
+ try {
115
+ const response = await axios.post(config.data_url, postBody);
116
+ setIsLoading(false);
117
+
118
+ if (response?.data) {
119
+ setData(response.data?.DATA);
120
+ setCount(response.data?.PAGESNUM);
121
+ setTotalPages(Math.ceil(response.data?.PAGESNUM / itemsPerPage) || 1);
122
+ } else {
123
+ setInvalidConfig(true);
124
+ }
125
+ } catch (error) {
126
+ setIsLoading(false);
127
+ setErrors((prev) => [
128
+ ...prev,
129
+ {
130
+ title: "Erreur",
131
+ message: "Une erreur est survenue",
132
+ },
133
+ ]);
134
+ }
135
+ };
136
+
137
+ const fetchFilterOptions = async (filter: any, signal?: AbortSignal) => {
138
+ if (filter.type === "Select" && filter.urlSource) {
139
+ try {
140
+ const response = await axios.get(filter.urlSource, { signal });
141
+ if (response?.status === 200 && response?.data) {
142
+ const options = Array.isArray(response.data) ? response.data : [];
143
+ setFilterOptions((prev) =>
144
+ prev.map((f) => (f.name === filter.name ? { ...f, options } : f))
145
+ );
146
+ }
147
+ } catch (error) {
148
+ if (!axios.isCancel(error)) {
149
+ setErrors((prev) => [
150
+ ...prev,
151
+ {
152
+ title: "Erreur",
153
+ message: `Une erreur est survenue lors de la récupération des options pour ${filter.name}`,
154
+ },
155
+ ]);
156
+ console.error(`Error fetching options for ${filter.name}:`, error);
157
+ }
158
+ }
159
+ }
160
+ };
161
+
162
+ if (invalidConfig) {
163
+ return (
164
+ <div
165
+ style={{
166
+ display: "flex",
167
+ justifyContent: "center",
168
+ alignItems: "center",
169
+ height: "100vh",
170
+ }}
171
+ >
172
+ <div style={{ position: "absolute" }}>
173
+ <svg
174
+ className="h-5 w-5 mx-auto mb-2"
175
+ xmlns="http://www.w3.org/2000/svg"
176
+ viewBox="0 0 24 24"
177
+ fill="red"
178
+ >
179
+ <path d="M18.3 5.71a.996.996 0 00-1.41 0L12 10.59 7.11 5.7A.996.996 0 105.7 7.11L10.59 12 5.7 16.89a.996.996 0 101.41 1.41L12 13.41l4.89 4.89a.996.996 0 101.41-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" />
180
+ </svg>
181
+ <div>Invalid configuration</div>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ if (!config) {
188
+ return (
189
+ <div
190
+ style={{
191
+ display: "flex",
192
+ justifyContent: "center",
193
+ alignItems: "center",
194
+ height: "100vh",
195
+ }}
196
+ >
197
+ <div style={{ position: "absolute" }}>
198
+ <svg
199
+ className="animate-spin h-5 w-5 mx-auto mb-2"
200
+ xmlns="http://www.w3.org/2000/svg"
201
+ fill="none"
202
+ viewBox="0 0 24 24"
203
+ >
204
+ <circle
205
+ className="opacity-25"
206
+ cx="12"
207
+ cy="12"
208
+ r="10"
209
+ stroke="currentColor"
210
+ strokeWidth="4"
211
+ ></circle>
212
+ <path
213
+ className="opacity-75"
214
+ fill="currentColor"
215
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
216
+ ></path>
217
+ </svg>
218
+ <div>Loading configuration...</div>
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ return (
225
+ <div className="flex flex-col gap-4 p-4 max-w-[1300px] mx-auto mt-8 mb-">
226
+ <PageHeader
227
+ title={config.title}
228
+ configFilters={filterOptions}
229
+ filters={filters}
230
+ setFilters={setFilters}
231
+ onSearch={() => getData()}
232
+ count={count}
233
+ isLoading={isLoading}
234
+ setCurrentPage={setCurrentPage}
235
+ />
236
+ {errors.map((error, index) => (
237
+ <Alert key={index} variant="destructive">
238
+ <AlertCircle className="h-4 w-4" />
239
+ <AlertTitle>{error.title}</AlertTitle>
240
+ <AlertDescription>{error.message}</AlertDescription>
241
+ </Alert>
242
+ ))}
243
+ <CustomTable data={data} columns={config.colomns} isLoading={isLoading} />
244
+ <Pagination
245
+ currentPage={currentPage}
246
+ totalPages={totalPages}
247
+ onPageChange={setCurrentPage}
248
+ />
249
+ {children}
250
+ </div>
251
+ );
252
+ }
253
+
254
+ export {MobigridModule as default};
package/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mobigrid-module",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "",
5
- "main": "dist/index.tsx",
6
5
  "type": "module",
6
+ "main": "dist/index.tsx",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "build": "rollup -c"
@@ -65,6 +65,10 @@
65
65
  },
66
66
  "types": "dist/index.d",
67
67
  "files": [
68
- "dist"
68
+ "dist",
69
+ "index.tsx",
70
+ "index.d.ts",
71
+ "components",
72
+ "lib"
69
73
  ]
70
74
  }