pejay-ui 1.0.4 → 1.1.0

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 CHANGED
@@ -129,3 +129,10 @@ Below is the list of components you can add. Each has a copyable command block w
129
129
  ```bash
130
130
  npx pejay-ui add dropdown/multiselect-input
131
131
  ```
132
+
133
+ ### Scaffolds & Templates
134
+
135
+ * **`tanstack-query-client`**: Bare-bone TanStack Query client and context provider setup, copied directly into your project's `src/tanstack-query/`.
136
+ ```bash
137
+ npx pejay-ui add tanstack-query-client
138
+ ```
package/bin/cli.js CHANGED
@@ -21,6 +21,23 @@ const prompt = async (questions) => {
21
21
  return inquirer.default.prompt(questions);
22
22
  };
23
23
 
24
+ // Helper to get all files recursively from a directory
25
+ const getFilesRecursively = async (dir) => {
26
+ let results = [];
27
+ const list = await fs.readdir(dir);
28
+ for (const file of list) {
29
+ const filePath = path.join(dir, file);
30
+ const stat = await fs.stat(filePath);
31
+ if (stat && stat.isDirectory()) {
32
+ const subFiles = await getFilesRecursively(filePath);
33
+ results = results.concat(subFiles);
34
+ } else {
35
+ results.push(filePath);
36
+ }
37
+ }
38
+ return results;
39
+ };
40
+
24
41
  program
25
42
  .name("pejay-ui")
26
43
  .description("CLI to initialize, add, and remove React UI components")
@@ -178,7 +195,12 @@ program
178
195
  }
179
196
 
180
197
  // 3. Process & Copy Component Files
181
- const targetDir = path.join(cwd, config.baseDir, "components", componentData.category);
198
+ let targetDir;
199
+ if (componentData.category === "tanstack-query") {
200
+ targetDir = path.join(cwd, "src", "tanstack-query");
201
+ } else {
202
+ targetDir = path.join(cwd, config.baseDir, "components", componentData.category);
203
+ }
182
204
  const outputExt = isTsProject ? "tsx" : "jsx";
183
205
 
184
206
  // Determine list of files to copy
@@ -192,29 +214,70 @@ program
192
214
  process.exit(1);
193
215
  }
194
216
 
195
- const filename = path.basename(srcFilePath).replace(/\.(tsx|ts)$/, (match) => {
196
- return match === ".tsx" ? `.${outputExt}` : `.${isTsProject ? "ts" : "js"}`;
197
- });
198
- const targetFile = path.join(targetDir, filename);
199
-
200
- let componentCode = await fs.readFile(templateSrc, "utf-8");
217
+ const isDir = (await fs.stat(templateSrc)).isDirectory();
218
+ if (isDir) {
219
+ const allFiles = await getFilesRecursively(templateSrc);
220
+ for (const file of allFiles) {
221
+ const relativePath = path.relative(templateSrc, file);
222
+ const filename = relativePath.replace(/\.(tsx|ts)$/, (match) => {
223
+ return match === ".tsx" ? `.${outputExt}` : `.${isTsProject ? "ts" : "js"}`;
224
+ });
225
+ const targetFile = path.join(targetDir, filename);
226
+
227
+ let componentCode = await fs.readFile(file, "utf-8");
228
+
229
+ const fileDir = path.dirname(targetFile);
230
+ const relativeToUtils = path.relative(fileDir, targetUtilsDir).replace(/\\/g, "/");
231
+ const cnImportPath = isTsProject
232
+ ? `${relativeToUtils}/cn`
233
+ : `${relativeToUtils}/cn.js`;
234
+ componentCode = componentCode.replace(/@\/utils\/cn/g, cnImportPath);
201
235
 
202
- // Replace `@/utils/cn` with relative path to the local utils/cn folder
203
- const cnImportPath = isTsProject ? "../../utils/cn" : "../../utils/cn.js";
204
- componentCode = componentCode.replace(/@\/utils\/cn/g, cnImportPath);
236
+ if (!isTsProject) {
237
+ const transformed = babel.transformSync(componentCode, {
238
+ presets: ["@babel/preset-typescript"],
239
+ filename: path.basename(file),
240
+ });
241
+ componentCode = transformed?.code || componentCode;
242
+ }
205
243
 
206
- if (!isTsProject) {
207
- const transformed = babel.transformSync(componentCode, {
208
- presets: ["@babel/preset-typescript"],
209
- filename: path.basename(srcFilePath),
244
+ await fs.ensureDir(path.dirname(targetFile));
245
+ await fs.writeFile(targetFile, componentCode, "utf-8");
246
+
247
+ const relativeTargetFile = path.relative(cwd, targetFile).replace(/\\/g, "/");
248
+ console.log(`✅ Created ${relativeTargetFile}`);
249
+ installedFiles.push(path.relative(path.join(cwd, config.baseDir), targetFile).replace(/\\/g, "/"));
250
+ }
251
+ } else {
252
+ const filename = path.basename(srcFilePath).replace(/\.(tsx|ts)$/, (match) => {
253
+ return match === ".tsx" ? `.${outputExt}` : `.${isTsProject ? "ts" : "js"}`;
210
254
  });
211
- componentCode = transformed?.code || componentCode;
212
- }
255
+ const targetFile = path.join(targetDir, filename);
256
+
257
+ let componentCode = await fs.readFile(templateSrc, "utf-8");
258
+
259
+ const fileDir = path.dirname(targetFile);
260
+ const relativeToUtils = path.relative(fileDir, targetUtilsDir).replace(/\\/g, "/");
261
+ const cnImportPath = isTsProject
262
+ ? `${relativeToUtils}/cn`
263
+ : `${relativeToUtils}/cn.js`;
264
+ componentCode = componentCode.replace(/@\/utils\/cn/g, cnImportPath);
265
+
266
+ if (!isTsProject) {
267
+ const transformed = babel.transformSync(componentCode, {
268
+ presets: ["@babel/preset-typescript"],
269
+ filename: path.basename(srcFilePath),
270
+ });
271
+ componentCode = transformed?.code || componentCode;
272
+ }
213
273
 
214
- await fs.ensureDir(targetDir);
215
- await fs.writeFile(targetFile, componentCode, "utf-8");
216
- console.log(`✅ Created components/${componentData.category}/${filename}`);
217
- installedFiles.push(path.join("components", componentData.category, filename));
274
+ await fs.ensureDir(targetDir);
275
+ await fs.writeFile(targetFile, componentCode, "utf-8");
276
+
277
+ const relativeTargetFile = path.relative(cwd, targetFile).replace(/\\/g, "/");
278
+ console.log(`✅ Created ${relativeTargetFile}`);
279
+ installedFiles.push(path.relative(path.join(cwd, config.baseDir), targetFile).replace(/\\/g, "/"));
280
+ }
218
281
  }
219
282
 
220
283
  // 4. Update State tracking in config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pejay-ui",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "react ui components",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "dependencies": {
23
23
  "@babel/core": "^7.24.0",
24
24
  "@babel/preset-typescript": "^7.24.0",
25
+ "@tanstack/react-query": "^5.100.14",
25
26
  "commander": "^12.0.0",
26
27
  "fs-extra": "^11.0.0",
27
28
  "inquirer": "^9.0.0"
package/registry.json CHANGED
@@ -2,328 +2,174 @@
2
2
  "button": {
3
3
  "name": "Button",
4
4
  "category": "button",
5
- "files": [
6
- "templates/button/Button.tsx",
7
- "templates/button/tooltip.tsx"
8
- ],
9
- "utils": [
10
- "cn.ts"
11
- ],
12
- "peerDependencies": [
13
- "clsx",
14
- "tailwind-merge"
15
- ]
5
+ "files": ["templates/button/Button.tsx", "templates/button/tooltip.tsx"],
6
+ "utils": ["cn.ts"],
7
+ "peerDependencies": ["clsx", "tailwind-merge"]
16
8
  },
17
9
  "form/input": {
18
10
  "name": "Input",
19
11
  "category": "form",
20
- "files": [
21
- "templates/form/input.tsx"
22
- ],
23
- "utils": [
24
- "cn.ts"
25
- ],
26
- "peerDependencies": [
27
- "clsx",
28
- "tailwind-merge",
29
- "lucide-react"
30
- ]
12
+ "files": ["templates/form/input.tsx"],
13
+ "utils": ["cn.ts"],
14
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
31
15
  },
32
16
  "form/amount-input": {
33
17
  "name": "AmountInput",
34
18
  "category": "form",
35
- "files": [
36
- "templates/form/amount-input.tsx"
37
- ],
38
- "utils": [
39
- "cn.ts"
40
- ],
41
- "peerDependencies": [
42
- "clsx",
43
- "tailwind-merge",
44
- "lucide-react"
45
- ]
19
+ "files": ["templates/form/amount-input.tsx"],
20
+ "utils": ["cn.ts"],
21
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
46
22
  },
47
23
  "form/checkbox": {
48
24
  "name": "Checkbox",
49
25
  "category": "form",
50
- "files": [
51
- "templates/form/checkbox.tsx"
52
- ],
53
- "utils": [
54
- "cn.ts"
55
- ],
56
- "peerDependencies": [
57
- "clsx",
58
- "tailwind-merge",
59
- "lucide-react"
60
- ]
26
+ "files": ["templates/form/checkbox.tsx"],
27
+ "utils": ["cn.ts"],
28
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
61
29
  },
62
30
  "form/checkbox-group": {
63
31
  "name": "CheckboxGroup",
64
32
  "category": "form",
65
- "files": [
66
- "templates/form/checkbox-group.tsx"
67
- ],
68
- "utils": [
69
- "cn.ts"
70
- ],
71
- "peerDependencies": [
72
- "clsx",
73
- "tailwind-merge"
74
- ],
75
- "dependencies": [
76
- "form/checkbox"
77
- ]
33
+ "files": ["templates/form/checkbox-group.tsx"],
34
+ "utils": ["cn.ts"],
35
+ "peerDependencies": ["clsx", "tailwind-merge"],
36
+ "dependencies": ["form/checkbox"]
78
37
  },
79
38
  "form/date-picker": {
80
39
  "name": "DatePicker",
81
40
  "category": "form",
82
- "files": [
83
- "templates/form/date-picker.tsx"
84
- ],
85
- "utils": [
86
- "cn.ts"
87
- ],
41
+ "files": ["templates/form/date-picker.tsx"],
42
+ "utils": ["cn.ts"],
88
43
  "peerDependencies": [
89
44
  "clsx",
90
45
  "tailwind-merge",
91
46
  "lucide-react",
92
47
  "@floating-ui/react"
93
48
  ],
94
- "dependencies": [
95
- "select-dropdown/select-input"
96
- ]
49
+ "dependencies": ["select-dropdown/select-input"]
97
50
  },
98
51
  "form/date-range-picker": {
99
52
  "name": "DateRangePicker",
100
53
  "category": "form",
101
- "files": [
102
- "templates/form/date-range-picker.tsx"
103
- ],
104
- "utils": [
105
- "cn.ts"
106
- ],
54
+ "files": ["templates/form/date-range-picker.tsx"],
55
+ "utils": ["cn.ts"],
107
56
  "peerDependencies": [
108
57
  "clsx",
109
58
  "tailwind-merge",
110
59
  "lucide-react",
111
60
  "@floating-ui/react"
112
61
  ],
113
- "dependencies": [
114
- "select-dropdown/select-input"
115
- ]
62
+ "dependencies": ["select-dropdown/select-input"]
116
63
  },
117
64
  "form/email-input": {
118
65
  "name": "EmailInput",
119
66
  "category": "form",
120
- "files": [
121
- "templates/form/email-input.tsx"
122
- ],
123
- "utils": [
124
- "cn.ts"
125
- ],
126
- "peerDependencies": [
127
- "clsx",
128
- "tailwind-merge",
129
- "lucide-react"
130
- ]
67
+ "files": ["templates/form/email-input.tsx"],
68
+ "utils": ["cn.ts"],
69
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
131
70
  },
132
71
  "form/file-input": {
133
72
  "name": "FileInput",
134
73
  "category": "form",
135
- "files": [
136
- "templates/form/file-input.tsx"
137
- ],
138
- "utils": [
139
- "cn.ts"
140
- ],
141
- "peerDependencies": [
142
- "clsx",
143
- "tailwind-merge",
144
- "lucide-react"
145
- ]
74
+ "files": ["templates/form/file-input.tsx"],
75
+ "utils": ["cn.ts"],
76
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
146
77
  },
147
78
  "form/number-input": {
148
79
  "name": "NumberInput",
149
80
  "category": "form",
150
- "files": [
151
- "templates/form/number-input.tsx"
152
- ],
153
- "utils": [
154
- "cn.ts"
155
- ],
156
- "peerDependencies": [
157
- "clsx",
158
- "tailwind-merge",
159
- "lucide-react"
160
- ]
81
+ "files": ["templates/form/number-input.tsx"],
82
+ "utils": ["cn.ts"],
83
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
161
84
  },
162
85
  "form/password-input": {
163
86
  "name": "PasswordInput",
164
87
  "category": "form",
165
- "files": [
166
- "templates/form/password-input.tsx"
167
- ],
168
- "utils": [
169
- "cn.ts"
170
- ],
171
- "peerDependencies": [
172
- "clsx",
173
- "tailwind-merge",
174
- "lucide-react"
175
- ]
88
+ "files": ["templates/form/password-input.tsx"],
89
+ "utils": ["cn.ts"],
90
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
176
91
  },
177
92
  "form/phone-input": {
178
93
  "name": "PhoneInput",
179
94
  "category": "form",
180
- "files": [
181
- "templates/form/phone-input.tsx"
182
- ],
183
- "utils": [
184
- "cn.ts"
185
- ],
186
- "peerDependencies": [
187
- "clsx",
188
- "tailwind-merge",
189
- "lucide-react"
190
- ]
95
+ "files": ["templates/form/phone-input.tsx"],
96
+ "utils": ["cn.ts"],
97
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
191
98
  },
192
99
  "form/radio": {
193
100
  "name": "Radio",
194
101
  "category": "form",
195
- "files": [
196
- "templates/form/radio.tsx"
197
- ],
198
- "utils": [
199
- "cn.ts"
200
- ],
201
- "peerDependencies": [
202
- "clsx",
203
- "tailwind-merge"
204
- ]
102
+ "files": ["templates/form/radio.tsx"],
103
+ "utils": ["cn.ts"],
104
+ "peerDependencies": ["clsx", "tailwind-merge"]
205
105
  },
206
106
  "form/radio-group": {
207
107
  "name": "RadioGroup",
208
108
  "category": "form",
209
- "files": [
210
- "templates/form/radio-group.tsx"
211
- ],
212
- "utils": [
213
- "cn.ts"
214
- ],
215
- "peerDependencies": [
216
- "clsx",
217
- "tailwind-merge"
218
- ],
219
- "dependencies": [
220
- "form/radio"
221
- ]
109
+ "files": ["templates/form/radio-group.tsx"],
110
+ "utils": ["cn.ts"],
111
+ "peerDependencies": ["clsx", "tailwind-merge"],
112
+ "dependencies": ["form/radio"]
222
113
  },
223
114
  "form/range-slider": {
224
115
  "name": "RangeSlider",
225
116
  "category": "form",
226
- "files": [
227
- "templates/form/range-slider.tsx"
228
- ],
229
- "utils": [
230
- "cn.ts"
231
- ],
232
- "peerDependencies": [
233
- "clsx",
234
- "tailwind-merge"
235
- ]
117
+ "files": ["templates/form/range-slider.tsx"],
118
+ "utils": ["cn.ts"],
119
+ "peerDependencies": ["clsx", "tailwind-merge"]
236
120
  },
237
121
  "form/switch": {
238
122
  "name": "Switch",
239
123
  "category": "form",
240
- "files": [
241
- "templates/form/switch.tsx"
242
- ],
243
- "utils": [
244
- "cn.ts"
245
- ],
246
- "peerDependencies": [
247
- "clsx",
248
- "tailwind-merge"
249
- ]
124
+ "files": ["templates/form/switch.tsx"],
125
+ "utils": ["cn.ts"],
126
+ "peerDependencies": ["clsx", "tailwind-merge"]
250
127
  },
251
128
  "form/textarea": {
252
129
  "name": "Textarea",
253
130
  "category": "form",
254
- "files": [
255
- "templates/form/textarea.tsx"
256
- ],
257
- "utils": [
258
- "cn.ts"
259
- ],
260
- "peerDependencies": [
261
- "clsx",
262
- "tailwind-merge"
263
- ]
131
+ "files": ["templates/form/textarea.tsx"],
132
+ "utils": ["cn.ts"],
133
+ "peerDependencies": ["clsx", "tailwind-merge"]
264
134
  },
265
135
  "form/time-picker": {
266
136
  "name": "TimePicker",
267
137
  "category": "form",
268
- "files": [
269
- "templates/form/time-picker.tsx"
270
- ],
271
- "utils": [
272
- "cn.ts"
273
- ],
138
+ "files": ["templates/form/time-picker.tsx"],
139
+ "utils": ["cn.ts"],
274
140
  "peerDependencies": [
275
141
  "clsx",
276
142
  "tailwind-merge",
277
143
  "lucide-react",
278
144
  "@floating-ui/react"
279
145
  ],
280
- "dependencies": [
281
- "select-dropdown/select-input"
282
- ]
146
+ "dependencies": ["select-dropdown/select-input"]
283
147
  },
284
148
  "form/time-range-picker": {
285
149
  "name": "TimeRangePicker",
286
150
  "category": "form",
287
- "files": [
288
- "templates/form/time-range-picker.tsx"
289
- ],
290
- "utils": [
291
- "cn.ts"
292
- ],
151
+ "files": ["templates/form/time-range-picker.tsx"],
152
+ "utils": ["cn.ts"],
293
153
  "peerDependencies": [
294
154
  "clsx",
295
155
  "tailwind-merge",
296
156
  "lucide-react",
297
157
  "@floating-ui/react"
298
158
  ],
299
- "dependencies": [
300
- "select-dropdown/select-input"
301
- ]
159
+ "dependencies": ["select-dropdown/select-input"]
302
160
  },
303
161
  "form/url-input": {
304
162
  "name": "UrlInput",
305
163
  "category": "form",
306
- "files": [
307
- "templates/form/url-input.tsx"
308
- ],
309
- "utils": [
310
- "cn.ts"
311
- ],
312
- "peerDependencies": [
313
- "clsx",
314
- "tailwind-merge",
315
- "lucide-react"
316
- ]
164
+ "files": ["templates/form/url-input.tsx"],
165
+ "utils": ["cn.ts"],
166
+ "peerDependencies": ["clsx", "tailwind-merge", "lucide-react"]
317
167
  },
318
168
  "dropdown/select-input": {
319
169
  "name": "SelectInput",
320
170
  "category": "select-dropdown",
321
- "files": [
322
- "templates/select-dropdown/select-input.tsx"
323
- ],
324
- "utils": [
325
- "cn.ts"
326
- ],
171
+ "files": ["templates/select-dropdown/select-input.tsx"],
172
+ "utils": ["cn.ts"],
327
173
  "peerDependencies": [
328
174
  "clsx",
329
175
  "tailwind-merge",
@@ -334,17 +180,19 @@
334
180
  "dropdown/multiselect-input": {
335
181
  "name": "MultiselectInput",
336
182
  "category": "select-dropdown",
337
- "files": [
338
- "templates/select-dropdown/multiselect-input.tsx"
339
- ],
340
- "utils": [
341
- "cn.ts"
342
- ],
183
+ "files": ["templates/select-dropdown/multiselect-input.tsx"],
184
+ "utils": ["cn.ts"],
343
185
  "peerDependencies": [
344
186
  "clsx",
345
187
  "tailwind-merge",
346
188
  "lucide-react",
347
189
  "@floating-ui/react"
348
190
  ]
191
+ },
192
+ "tanstack-query-client": {
193
+ "name": "TanstackQueryClient",
194
+ "category": "tanstack-query",
195
+ "files": ["templates/scaffolds/tanstack-query"],
196
+ "peerDependencies": ["@tanstack/react-query"]
349
197
  }
350
198
  }
@@ -0,0 +1,68 @@
1
+ /*
2
+ #ANCHOR : TYPE:1 VITE PROJECT
3
+ const BASE_URL = import.meta.env.VITE_API_URL;
4
+
5
+ Environment variables must start with VITE_ to be exposed to client-side code in Vite.
6
+
7
+ ---------------------------------------------------------
8
+
9
+ #ANCHOR : TYPE:2 NEXT.JS / NON-VITE PROJECTS
10
+ const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
11
+
12
+ Environment variables must start with NEXT_PUBLIC_ to be available in Next.js browser code.
13
+
14
+ ---------------------------------------------------------
15
+ #ANCHOR
16
+ Vite doesn't automatically provide Node's process.env in browser code.
17
+
18
+ | Framework | Access Method | Public Prefix |
19
+ | --------------- | ------------------- | ---------------- |
20
+ | Next.js | `process.env.X` | `NEXT_PUBLIC_` |
21
+ | Vite | `import.meta.env.X` | `VITE_` |
22
+ | Node.js Backend | `process.env.X` | No prefix needed |
23
+
24
+ */
25
+
26
+ export const API_CONFIG = {
27
+ // NOTE: Change this BASE_URL to match your actual API endpoint
28
+ baseUrl: "http://localhost:5000/api",
29
+ timeout: 10000,
30
+ headers: {
31
+ "Accept": "application/json",
32
+ "Content-Type": "application/json",
33
+ },
34
+ } as const;
35
+
36
+ export const QUERY_CLIENT_CONFIG = {
37
+ defaultOptions: {
38
+ queries: {
39
+ // # NOTE: Global TanStack Query configurations
40
+ retry: 3, // Automatically retries failed requests 3 times on failure
41
+ refetchOnWindowFocus: false, // Refetches stale active queries when the browser tab gets focused
42
+ staleTime: 1000 * 60 * 5, // Data is considered fresh for 5 minutes (prevents duplicate requests)
43
+ },
44
+ },
45
+ } as const;
46
+
47
+ /*
48
+ # NOTE: HOW AND WHERE TO USE QUERY_CLIENT_CONFIG
49
+
50
+ This configuration is imported and passed when initializing the QueryClient at the root of your application (e.g., in App.tsx, main.tsx, or layout.tsx):
51
+
52
+ ```typescript
53
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
54
+ import { QUERY_CLIENT_CONFIG } from "./api-base";
55
+
56
+ const queryClient = new QueryClient(QUERY_CLIENT_CONFIG);
57
+
58
+ export default function Providers({ children }) {
59
+ return (
60
+ <QueryClientProvider client={queryClient}>
61
+ {children}
62
+ </QueryClientProvider>
63
+ );
64
+ }
65
+ ```
66
+ */
67
+
68
+
@@ -0,0 +1,15 @@
1
+ import { ModuleMutations } from "./module";
2
+
3
+ export const apiMutations = {
4
+ module: ModuleMutations,
5
+ };
6
+
7
+ export * from "./module";
8
+
9
+ /*
10
+ # NOTE : here you export all the mutations from the module folder so it will be easy to import in components
11
+
12
+ const queryClient = useQueryClient();
13
+ const mutation_name_example = useMutation(ModuleMutations.create_query_name_example(queryClient));
14
+ mutation_name_example.mutate({input_name: data})
15
+ */
@@ -0,0 +1,274 @@
1
+ import { ModuleQueries } from "./module";
2
+
3
+ export const apiQueries = {
4
+ module: ModuleQueries,
5
+ };
6
+
7
+ export * from "./module";
8
+
9
+ /*
10
+ # NOTE : here you export all the queries from the module folder so it will be easy to import in components
11
+
12
+ const { data: name } = useQuery(ModuleQueries.fetch_query_name_example());
13
+ const { data: name } = useSuspenseQuery(ModuleQueries.fetch_query_name_example());
14
+
15
+ // Example with parameters & request cancellation:
16
+ // const { data } = useQuery(ModuleQueries.fetch_query_with_params_example({ search: 'query', filter: 'active' }));
17
+
18
+ ---------------------------------------------------------------------------------------------------
19
+
20
+ | Feature | `useQuery` | `useSuspenseQuery` |
21
+ | ------------------- | ---------------------------------------------- | --------------------------------------------------- |
22
+ | Loading state | Returns `isLoading`, `isPending`, `isFetching` | Does **not** return loading state |
23
+ | Data type | `data` can be `undefined` | `data` is guaranteed to exist when rendered |
24
+ | Error handling | Use `error` from hook | Error is caught by React Error Boundary |
25
+ | Loading UI | Handle manually (`if (isLoading)`) | Handled by React `<Suspense>` fallback |
26
+ | Component rendering | Renders immediately, then fetches | Suspends rendering until data is ready |
27
+ | Setup complexity | Simpler | Requires `<Suspense>` and usually an Error Boundary |
28
+ | Best for | Most applications | Apps fully using React Suspense |
29
+
30
+ ----------------------------------------------------------------------------------------------------
31
+
32
+ # NOTE : Example for useQuery
33
+ const ModuleComponent = () => {
34
+
35
+ const { data, isLoading, error } = useQuery(
36
+ ModuleQueries.fetch_query_name_example()
37
+ );
38
+
39
+ if (isLoading) return <FallBackComponent />;
40
+ if (error) return <ErrorComponent />;
41
+
42
+ return <ModuleComponent data={data} />;
43
+
44
+ }
45
+
46
+ -----------------------------------------------------------------------------------------------------
47
+
48
+ # NOTE : Example for useSuspenseQuery
49
+
50
+ const ModuleComponent = () => {
51
+
52
+ const { data } = useSuspenseQuery(
53
+ ModuleQueries.fetch_query_name_example()
54
+ );
55
+
56
+ return <ModuleComponent data={data} />;
57
+
58
+ }
59
+
60
+ # NOTE : Wrapping
61
+ <Suspense fallback={<FallBackComponent />}>
62
+ <ModuleComponent />
63
+ </Suspense>
64
+
65
+ ---------------------------------------------------------------------------------------------------
66
+
67
+ Use useQuery when you want to manage loading and errors inside the component.
68
+ Use useSuspenseQuery when your app already uses React Suspense and you want cleaner components with guaranteed data.
69
+
70
+ ---------------------------------------------------------------------------------------------------
71
+
72
+ # NOTE : Example for useInfiniteQuery (Infinite Scrolling)
73
+
74
+ 1. In queries.ts, define the option:
75
+ export const ModuleQueries = {
76
+ fetch_infinite_query_example: () =>
77
+ infiniteQueryOptions({
78
+ queryKey: [...ModuleKeys.module(), "infinite"] as const,
79
+ queryFn: async ({ pageParam = 1 }) => {
80
+ const raw = await ModuleService.get_infinite_query_example(pageParam as number);
81
+ return raw as { data: any[]; meta: { current_page: number; last_page: number } };
82
+ },
83
+ getNextPageParam: (raw) => {
84
+ // E.g., if page-based (extracting from 'meta' object):
85
+ const { current_page, last_page } = raw?.meta || {};
86
+ return current_page < last_page ? current_page + 1 : undefined;
87
+ },
88
+ initialPageParam: 1,
89
+ select: (raw) => ({
90
+ pages: raw.pages.map((pageData: any) => ({
91
+ ...pageData,
92
+ data: ModuleMappers.fetch_infinite_query_example(pageData.data),
93
+ })),
94
+ pageParams: raw.pageParams,
95
+ }),
96
+ })
97
+ }
98
+
99
+ 2. In your Component:
100
+ import { useInfiniteQuery } from "@tanstack/react-query";
101
+ import { useEffect } from "react";
102
+ import { useInView } from "react-intersection-observer"; // optional, or use standard scroll listener
103
+ import { ModuleQueries } from "./queries";
104
+
105
+ const InfiniteListComponent = () => {
106
+ const {
107
+ data,
108
+ fetchNextPage,
109
+ hasNextPage,
110
+ isFetchingNextPage,
111
+ isLoading,
112
+ error
113
+ } = useInfiniteQuery(ModuleQueries.fetch_infinite_query_example());
114
+
115
+ // Optional hook to detect if target element is visible in the viewport
116
+ const { ref, inView } = useInView();
117
+
118
+ useEffect(() => {
119
+ if (inView && hasNextPage && !isFetchingNextPage) {
120
+ fetchNextPage();
121
+ }
122
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
123
+
124
+ if (isLoading) return <Loading />;
125
+ if (error) return <Error />;
126
+
127
+ // Flatten the paginated data pages into a single flat array
128
+ const items = data ? data.pages.flatMap((page) => page.data) : [];
129
+
130
+ return (
131
+ <div>
132
+ <ul>
133
+ {items.map((item) => (
134
+ <li key={item.id}>{item.name}</li>
135
+ ))}
136
+ </ul>
137
+
138
+ // This invisible boundary element triggers loading the next page when scrolled into view
139
+ <div ref={ref} style={{ height: "10px" }}>
140
+ {isFetchingNextPage ? "Loading more..." : ""}
141
+ </div>
142
+ </div>
143
+ );
144
+ };
145
+
146
+ ---------------------------------------------------------------------------------------------------
147
+
148
+ # HOW INFINITE SCROLLING WORKS:
149
+ 1. **Initial Load:** `useInfiniteQuery` calls `queryFn` using `initialPageParam` (usually page `1`). The response is saved in `data.pages[0]`.
150
+ 2. **Next Page Calculation:** `getNextPageParam` is called with the last fetched page data. It checks if there is more data (e.g., if `current_page < last_page`). If returned, `hasNextPage` becomes `true`.
151
+ 3. **Scroll Trigger:** When the user scrolls down, an observer (like `react-intersection-observer` on the bottom `div` element) fires an `inView` event.
152
+ 4. **Fetch Call:** If `inView`, `hasNextPage`, and not currently loading, we call `fetchNextPage()`.
153
+ 5. **Caching & Appending:** TanStack Query triggers `queryFn` passing the next page number (e.g., `2`) as the new `pageParam`. The new data is fetched and appended as a new array element inside `data.pages` (e.g., `data.pages[1]`). The UI re-renders with the flattened items list.
154
+
155
+ ---------------------------------------------------------------------------------------------------
156
+
157
+ # NOTE : Example for Query Parameters (Filters & Cancellation)
158
+
159
+ This example shows a component using query parameters (including an array of brands) with automatic request cancellation:
160
+
161
+ ```typescript
162
+ import { useState } from "react";
163
+ import { useQuery } from "@tanstack/react-query";
164
+ import { ModuleQueries } from "./queries";
165
+ import { useDebounce } from "@/hooks/use-debounce"; // custom debounce hook
166
+
167
+ const FilteredProductList = () => {
168
+ const [search, setSearch] = useState("");
169
+ const [selectedBrands, setSelectedBrands] = useState<string[]>([]); // e.g., ["samsung", "apple"]
170
+ const [sortBy, setSortBy] = useState("price_asc"); // Single key-value parameter example
171
+
172
+ // 1. Debounce fast inputs (like keystrokes) to prevent hammering the server
173
+ const debouncedSearch = useDebounce(search, 300);
174
+
175
+ // 2. Combine your states. Any change to these properties will update the queryKey
176
+ const filters = {
177
+ search: debouncedSearch,
178
+ brands: selectedBrands, // Array will be serialized as "brands=samsung&brands=apple" by the service
179
+ sort: sortBy, // Single key-value parameter (e.g., "price_asc")
180
+ };
181
+
182
+ // 3. Pass the filter object directly to the query option builder
183
+ const { data, isLoading, isFetching, error } = useQuery(
184
+ ModuleQueries.fetch_query_with_params_example(filters)
185
+ );
186
+
187
+ const toggleBrand = (brand: string) => {
188
+ setSelectedBrands(prev =>
189
+ prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]
190
+ );
191
+ };
192
+
193
+ return (
194
+ <div>
195
+ <input
196
+ type="text"
197
+ value={search}
198
+ onChange={(e) => setSearch(e.target.value)}
199
+ placeholder="Search mobiles..."
200
+ />
201
+
202
+ <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
203
+ <option value="price_asc">Price: Low to High</option>
204
+ <option value="price_desc">Price: High to Low</option>
205
+ </select>
206
+
207
+ <div>
208
+ <label>
209
+ <input
210
+ type="checkbox"
211
+ checked={selectedBrands.includes("samsung")}
212
+ onChange={() => toggleBrand("samsung")}
213
+ /> Samsung
214
+ </label>
215
+ <label>
216
+ <input
217
+ type="checkbox"
218
+ checked={selectedBrands.includes("apple")}
219
+ onChange={() => toggleBrand("apple")}
220
+ /> Apple
221
+ </label>
222
+ </div>
223
+
224
+ {isFetching && <span>Updating results (Old requests cancelled automatically)...</span>}
225
+
226
+ {isLoading ? (
227
+ <p>Loading...</p>
228
+ ) : error ? (
229
+ <p>Error loading items</p>
230
+ ) : (
231
+ <ul>
232
+ {data?.map((item: any) => (
233
+ <li key={item.id}>{item.name}</li>
234
+ ))}
235
+ </ul>
236
+ )}
237
+ </div>
238
+ );
239
+ };
240
+ ```
241
+
242
+ ---------------------------------------------------------------------------------------------------
243
+
244
+ # NOTE: WHY WE DO NOT GET DUPLICATE KEY ERRORS IN FRONTEND STATE
245
+ Even though the final URL repeats the same key (e.g., `?brands=samsung&brands=apple`),
246
+ your frontend React state or router object only has one clean unique key mapping to an array:
247
+ `const filters = { brands: ["samsung", "apple"] };`
248
+
249
+ The service layer safely iterates over the array and appends the items individually to the query parameters (`queryParams.append`).
250
+ This keeps your frontend state easy to manage without causing duplicate key errors.
251
+
252
+
253
+
254
+
255
+
256
+
257
+ useEffect(() => {
258
+ // 1. Get query string from URL (or SessionStorage if URL is empty)
259
+ const searchParams = new URLSearchParams(window.location.search);
260
+ // 2. Parse the single values
261
+ const searchVal = searchParams.get("search") || "";
262
+ const sortVal = searchParams.get("sort") || "price_asc";
263
+ // 3. Parse the repeated values as an array
264
+ const brandsVal = searchParams.getAll("brands"); // returns ["samsung", "apple"]
265
+ // 4. Populate your component state
266
+ setSearch(searchVal);
267
+ setSortBy(sortVal);
268
+ setSelectedBrands(brandsVal);
269
+ }, []);
270
+
271
+ */
272
+
273
+
274
+
@@ -0,0 +1,63 @@
1
+ import { API_CONFIG } from "./api-base";
2
+
3
+ //NOTE: apiRequest: This is the core function that all other methods use.
4
+ export async function apiRequest<T>(
5
+ endpoint: string,
6
+ options: RequestInit = {},
7
+ ) {
8
+ // # NOTE: Retrieve your authentication token dynamically (e.g., from localStorage or cookies)
9
+ // const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
10
+ const token = null;
11
+
12
+ const response = await fetch(`${API_CONFIG.baseUrl}${endpoint}`, {
13
+ ...options,
14
+ headers: {
15
+ ...API_CONFIG.headers,
16
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
17
+ ...options.headers,
18
+ },
19
+ });
20
+
21
+ if (!response.ok) throw new Error(`Error Code ${response.status}`);
22
+ return (await response.json()) as T;
23
+ }
24
+
25
+ function withJsonBody(method: string, body: any, options?: RequestInit) {
26
+ return {
27
+ ...options,
28
+ method,
29
+ body: body === undefined ? undefined : JSON.stringify(body),
30
+ /*
31
+ # NOTE : fetch expects JSON bodies as strings
32
+ line this
33
+ body: '{"name":"John"}'
34
+ and not
35
+ body: data
36
+ in case of undefined - does a Undefined Body Check
37
+ body: "undefined"
38
+ */
39
+ } satisfies RequestInit;
40
+ }
41
+
42
+ /*
43
+
44
+ Contains all api type calls
45
+ Usage:
46
+ const data = await apiClient.get<User>("/users/1");
47
+ const data = await apiClient.post<User>("/users", { name: "John" });
48
+ const data = await apiClient.put<User>("/users/1", { name: "John" });
49
+ const data = await apiClient.patch<User>("/users/1", { name: "John" });
50
+ const data = await apiClient.delete<User>("/users/1");
51
+
52
+ */
53
+ export const apiClient = {
54
+ get: apiRequest,
55
+ post: <T>(endpoint: string, body?: any, options?: RequestInit) =>
56
+ apiRequest<T>(endpoint, withJsonBody("POST", body, options)),
57
+ put: <T>(endpoint: string, body?: any, options?: RequestInit) =>
58
+ apiRequest<T>(endpoint, withJsonBody("PUT", body, options)),
59
+ patch: <T>(endpoint: string, body?: any, options?: RequestInit) =>
60
+ apiRequest<T>(endpoint, withJsonBody("PATCH", body, options)),
61
+ delete: <T>(endpoint: string, options?: RequestInit) =>
62
+ apiRequest<T>(endpoint, { ...options, method: "DELETE" }),
63
+ } as const;
@@ -0,0 +1,12 @@
1
+ export { ModuleKeys } from "./keys";
2
+ export { ModuleMappers } from "./mappers";
3
+ export { ModuleService } from "./services";
4
+ export { ModuleQueries } from "./queries";
5
+ export { ModuleMutations } from "./mutations";
6
+
7
+ /*
8
+ # NOTE: here we can export types.ts as well if we defining types seperately in a file
9
+ export type * from "./types";
10
+ # NOTE : we can also export a mocks.ts file if we using mocks which contains dummy / fall back data
11
+ export { ModuleMocks } from "./mocks";
12
+ */
@@ -0,0 +1,17 @@
1
+ export const ModuleKeys = {
2
+ module: () => ["moduleName"] as const,
3
+ fetch_query_name_example: () => [...ModuleKeys.module(), "fetch"] as const,
4
+ create_query_name_example: () => [...ModuleKeys.module(), "create"] as const,
5
+ update_query_name_example: () => [...ModuleKeys.module(), "update"] as const,
6
+ delete_query_name_example: () => [...ModuleKeys.module(), "delete"] as const,
7
+ fetch_infinite_query_name_example: () => [...ModuleKeys.module(), "fetch-infinite"] as const,
8
+ fetch_query_with_params_example: (params: any) => [...ModuleKeys.module(), "fetch-with-params", params] as const,
9
+ fetch_query_by_id_example: (id: string) => [...ModuleKeys.module(), "fetch-by-id", id] as const,
10
+ fetch_query_combo_example: (id: string, params: any) => [...ModuleKeys.module(), "fetch-combo", id, params] as const,
11
+ };
12
+
13
+ /*
14
+ # NOTE: as const is used at end so that keys are immutable
15
+ # NOTE : ans insted of standard object keys usd as function so its easy and clean to use and maintain in larger scale
16
+ # NOTE : you can change *_name_example to your own query name
17
+ */
@@ -0,0 +1,15 @@
1
+ /*
2
+ # NOTE : you can change *_name_example to your own query name
3
+ # NOTE : you can change raw to your own data after manipulation
4
+ */
5
+
6
+ export const ModuleMappers = {
7
+ fetch_query_name_example(raw: any) {
8
+ const data = raw || "manipulate your data here and then return it";
9
+ return data;
10
+ },
11
+ fetch_infinite_query_example(raw: any) {
12
+ const data = raw || [];
13
+ return data;
14
+ },
15
+ };
@@ -0,0 +1,55 @@
1
+ import { mutationOptions, type QueryClient } from "@tanstack/react-query";
2
+ import { ModuleKeys } from "./keys";
3
+ import { ModuleService } from "./services";
4
+
5
+ export const ModuleMutations = {
6
+ create_query_name_example: (queryClient: QueryClient) =>
7
+ mutationOptions({
8
+ mutationFn: (newItem: any) =>
9
+ ModuleService.post_query_name_example(newItem),
10
+ onMutate: async (newItem: any) => {
11
+ /*
12
+ # OPTIMISTIC UPDATE TECHNIQUE:
13
+ 1. Cancel outgoing refetches so they don't overwrite our optimistic update.
14
+ 2. Snapshot the current cache value.
15
+ 3. Optimistically insert the new item into the cache.
16
+ 4. Return context containing the previous value for error rollbacks.
17
+ */
18
+ await queryClient.cancelQueries({ queryKey: ModuleKeys.fetch_query_name_example() });
19
+
20
+ const previousItems = queryClient.getQueryData(ModuleKeys.fetch_query_name_example());
21
+
22
+ queryClient.setQueryData(ModuleKeys.fetch_query_name_example(), (old: any) => {
23
+ return old ? [...old, newItem] : [newItem];
24
+ });
25
+
26
+ return { previousItems };
27
+ },
28
+ onSuccess: async () => {
29
+ /*
30
+ # NOTE: In case of mutations, invalidate query keys to refresh with fresh server data.
31
+ */
32
+ await queryClient.invalidateQueries({
33
+ queryKey: ModuleKeys.fetch_query_name_example(),
34
+ });
35
+ },
36
+ onError: (error, newItem, context: any) => {
37
+ /*
38
+ # ROLLBACK ON FAILURE:
39
+ If the mutation fails, rollback the cache to our snapshotted state.
40
+ */
41
+ if (context?.previousItems) {
42
+ queryClient.setQueryData(
43
+ ModuleKeys.fetch_query_name_example(),
44
+ context.previousItems
45
+ );
46
+ }
47
+ },
48
+ onSettled: async () => {
49
+ // Always refetch after error or success to keep server in sync
50
+ await queryClient.invalidateQueries({
51
+ queryKey: ModuleKeys.fetch_query_name_example(),
52
+ });
53
+ },
54
+ }),
55
+ };
@@ -0,0 +1,133 @@
1
+ import { queryOptions, infiniteQueryOptions, keepPreviousData } from "@tanstack/react-query";
2
+
3
+ import { ModuleKeys } from "./keys";
4
+ import { ModuleMappers } from "./mappers";
5
+ import { ModuleService } from "./services";
6
+
7
+ export const ModuleQueries = {
8
+ fetch_query_name_example: () =>
9
+ queryOptions({
10
+ queryKey: ModuleKeys.fetch_query_name_example(),
11
+ queryFn: () => {
12
+ /*
13
+ # NOTE: ModuleService.get_query_name_example() -> hits the api of query fetch or GET
14
+ Return the Promise directly to TanStack Query so it can natively handle retries and error states.
15
+ */
16
+ return ModuleService.get_query_name_example();
17
+ },
18
+ select: (raw) => {
19
+ /*
20
+ # NOTE: ModuleMappers.fetch_query_name_example() -> manipulates the data from api into desird format before returning to ui or page
21
+ Using the 'select' property memoizes this transformation (only runs when cached data changes).
22
+ */
23
+ return ModuleMappers.fetch_query_name_example(raw as any);
24
+ },
25
+ /*
26
+ # NOTE: keeps the last successfully fetched data visible on screen
27
+ while fetching new data or if a subsequent fetch fails (preventing layout flickers/empty states).
28
+ */
29
+ placeholderData: keepPreviousData,
30
+ }),
31
+
32
+ fetch_infinite_query_example: () =>
33
+ infiniteQueryOptions({
34
+ queryKey: ModuleKeys.fetch_infinite_query_name_example(),
35
+ queryFn: ({ pageParam = 1 }) => {
36
+ /*
37
+ # NOTE: ModuleService.get_infinite_query_example(pageParam) -> hits the api of query fetch or GET
38
+ Return the Promise directly to TanStack Query so it can natively handle retries and error states.
39
+ */
40
+ return ModuleService.get_infinite_query_example(pageParam as number);
41
+ },
42
+ getNextPageParam: (raw: any) => {
43
+ // --- Option A: Cursor-based Pagination ---
44
+ // return raw.nextCursor ?? undefined;
45
+
46
+ // --- Option B: Page-number Metadata Pagination (last_page, current_page) ---
47
+ // Access metadata from the 'meta' object returned in your API response
48
+ const { current_page, last_page } = raw?.meta || {};
49
+ const hasMore = current_page < last_page;
50
+ return hasMore ? current_page + 1 : undefined;
51
+ },
52
+ initialPageParam: 1,
53
+ select: (raw) => {
54
+ /*
55
+ # NOTE: ModuleMappers.fetch_infinite_query_example() -> manipulates the data from api into desird format before returning to ui or page
56
+ Using the 'select' property memoizes this transformation (only runs when cached data changes).
57
+ */
58
+ return {
59
+ pages: raw.pages.map((pageData: any) => ({
60
+ ...pageData,
61
+ data: ModuleMappers.fetch_infinite_query_example(pageData.data),
62
+ })),
63
+ pageParams: raw.pageParams,
64
+ };
65
+ },
66
+ placeholderData: keepPreviousData,
67
+ }),
68
+
69
+ fetch_query_with_params_example: (params: Record<string, any>) =>
70
+ queryOptions({
71
+ queryKey: ModuleKeys.fetch_query_with_params_example(params),
72
+ queryFn: ({ signal }) => {
73
+ /*
74
+ # NOTE: Pass the native 'signal' from the TanStack queryFn context down to the service call.
75
+ This enables automatic cancellation if query parameters change or the component unmounts.
76
+ */
77
+ return ModuleService.get_query_with_params_example(params, signal);
78
+ },
79
+ select: (raw) => {
80
+ // Reusing the same mapper example for consistency
81
+ return ModuleMappers.fetch_query_name_example(raw as any);
82
+ },
83
+ placeholderData: keepPreviousData,
84
+ }),
85
+
86
+ fetch_query_by_id_example: (id?: string | null) =>
87
+ queryOptions({
88
+ queryKey: ModuleKeys.fetch_query_by_id_example(id || ""),
89
+ queryFn: ({ signal }) => {
90
+ return ModuleService.get_query_by_id_example(id!, signal);
91
+ },
92
+ select: (raw) => {
93
+ return ModuleMappers.fetch_query_name_example(raw as any);
94
+ },
95
+ placeholderData: keepPreviousData,
96
+ /*
97
+ # NOTE: Conditional/Dependent Queries
98
+ Setting 'enabled: !!id' stops the query from executing automatically if the ID is missing (undefined, null, or empty).
99
+ */
100
+ enabled: !!id,
101
+ }),
102
+
103
+ fetch_query_combo_example: (id: string | null | undefined, params: Record<string, any>) =>
104
+ queryOptions({
105
+ queryKey: ModuleKeys.fetch_query_combo_example(id || "", params),
106
+ queryFn: ({ signal }) => {
107
+ return ModuleService.get_query_combo_example(id!, params, signal);
108
+ },
109
+ select: (raw) => {
110
+ return ModuleMappers.fetch_query_name_example(raw as any);
111
+ },
112
+ placeholderData: keepPreviousData,
113
+ enabled: !!id, // Automatically stops API call if ID is null/undefined
114
+ }),
115
+ };
116
+
117
+ /*
118
+ # NOTE: RAW DATA VS. MAPPED DATA & MEMOIZATION
119
+
120
+ 1. **Where Raw Data Lives:**
121
+ - The raw response returned by the API (`queryFn`) is stored unmodified inside the **TanStack Query Cache**.
122
+ - This represents the exact payload from your backend database/server.
123
+
124
+ 2. **Where Mapped Data Lives:**
125
+ - The mapped data is delivered directly to the **UI / React Component** consuming the hook.
126
+ - It is calculated on-the-fly by executing the `select` function on the cached raw data.
127
+
128
+ 3. **How Memoization Works (Performance Optimization):**
129
+ - The `select` function is **automatically memoized** by TanStack Query.
130
+ - It will ONLY re-run when the cached raw data changes.
131
+ - If the component re-renders for other reasons (e.g., local UI states, parent re-renders, or window focus checks), TanStack Query skips the mapper execution completely and returns the already memoized mapped data instantly.
132
+ */
133
+
@@ -0,0 +1,66 @@
1
+ import { apiClient } from "../client";
2
+
3
+ export const ModuleService = {
4
+ get_query_name_example: () => apiClient.get("/api_name/get"),
5
+ get_infinite_query_example: (page: number) =>
6
+ apiClient.get(`/api_name/list?page=${page}`),
7
+ get_query_with_params_example: (params: Record<string, any>, signal?: AbortSignal) => {
8
+ const queryParams = new URLSearchParams();
9
+ Object.entries(params).forEach(([key, value]) => {
10
+ if (Array.isArray(value)) {
11
+ value.forEach((val) => {
12
+ if (val !== undefined && val !== null && val !== "") {
13
+ queryParams.append(key, String(val));
14
+ }
15
+ });
16
+ } else if (value !== undefined && value !== null && value !== "") {
17
+ queryParams.set(key, String(value));
18
+ }
19
+ });
20
+ return apiClient.get(`/api_name/get-with-params?${queryParams.toString()}`, { signal });
21
+ },
22
+ get_query_by_id_example: (id: string, signal?: AbortSignal) =>
23
+ apiClient.get(`/api_name/get-by-id/${id}`, { signal }),
24
+ get_query_combo_example: (id: string, params: Record<string, any>, signal?: AbortSignal) => {
25
+ const queryParams = new URLSearchParams();
26
+ Object.entries(params).forEach(([key, value]) => {
27
+ if (Array.isArray(value)) {
28
+ value.forEach((val) => {
29
+ if (val !== undefined && val !== null && val !== "") {
30
+ queryParams.append(key, String(val));
31
+ }
32
+ });
33
+ } else if (value !== undefined && value !== null && value !== "") {
34
+ queryParams.set(key, String(value));
35
+ }
36
+ });
37
+ return apiClient.get(`/api_name/get-combo/${id}?${queryParams.toString()}`, { signal });
38
+ },
39
+ post_query_name_example: (input: any) =>
40
+ apiClient.post("/api_name/post", input),
41
+ patch_query_name_example: (input: any) =>
42
+ apiClient.patch("/api_name/patch", input),
43
+ delete_query_name_example: (id: string) =>
44
+ apiClient.delete(`/api_name/${id}`),
45
+ };
46
+
47
+ /*
48
+ # NOTE: you can change *_name_example to your own query name
49
+ # NOTE: you can change /api_name to your own api name
50
+ # NOTE: you can change input to your own data type
51
+ # NOTE: you can change id to your own data type
52
+
53
+ -------------------------------------------------------------------------
54
+
55
+ # NOTE: WHY WE USE URLSearchParams & THE ROLE OF THE SERVICE LAYER
56
+
57
+ 1. **Why we use `URLSearchParams`:**
58
+ - **Automatic URL-Encoding:** It automatically sanitizes special characters (like spaces, commas, or quotes) into browser-safe formats (e.g., space becomes `%20`), preventing broken URLs.
59
+ - **Dynamic Query String Building:** It generates the final string from a raw object dynamically (calling `.toString()` results in `key1=val1&key2=val2`).
60
+
61
+ 2. **Role of this Service Layer (`get_query_with_params_example`):**
62
+ - **Decoupled Contracts:** It keeps components "dumb" about network specifics. The component only needs to pass a clean JavaScript object containing the filters.
63
+ - **Centralization:** If endpoints change or parameter serialization logic needs to adjust in the future, it is managed in this single file rather than modifying multiple UI files.
64
+ */
65
+
66
+