@kaushalparajuli/react-crud-ui 1.0.11 → 1.0.13

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/bin/cli.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CRUD Module Generator CLI
4
+ *
5
+ * Usage: npx @kaushalparajuli/react-crud-ui generate <module-name>
6
+ * Example: npx @kaushalparajuli/react-crud-ui generate blog
7
+ *
8
+ * This will create:
9
+ * - src/pages/<module>/ModuleListPage.jsx
10
+ * - src/pages/<module>/ModuleFormPage.jsx
11
+ * - src/routes/modules/<module>Routes.js
12
+ * - src/stores/use<Module>Store.js
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+
18
+ const args = process.argv.slice(2);
19
+ const command = args[0];
20
+ const moduleName = args[1];
21
+
22
+ // Show help if no command
23
+ if (!command || command === '--help' || command === '-h') {
24
+ console.log(`
25
+ @kaushalparajuli/react-crud-ui CLI
26
+
27
+ Usage:
28
+ npx @kaushalparajuli/react-crud-ui generate <module-name>
29
+ npx @kaushalparajuli/react-crud-ui g <module-name>
30
+
31
+ Commands:
32
+ generate, g Generate a new CRUD module
33
+
34
+ Options:
35
+ --help, -h Show this help message
36
+ --version, -v Show version number
37
+
38
+ Examples:
39
+ npx @kaushalparajuli/react-crud-ui generate blog
40
+ npx @kaushalparajuli/react-crud-ui g product
41
+ npx @kaushalparajuli/react-crud-ui g user-profile
42
+ `);
43
+ process.exit(0);
44
+ }
45
+
46
+ // Show version
47
+ if (command === '--version' || command === '-v') {
48
+ try {
49
+ const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
50
+ console.log(packageJson.version);
51
+ } catch {
52
+ console.log('Unknown version');
53
+ }
54
+ process.exit(0);
55
+ }
56
+
57
+ // Handle generate command
58
+ if (command === 'generate' || command === 'g') {
59
+ if (!moduleName) {
60
+ console.error('❌ Please provide a module name');
61
+ console.log('Usage: npx @kaushalparajuli/react-crud-ui generate <module-name>');
62
+ console.log('Example: npx @kaushalparajuli/react-crud-ui generate blog');
63
+ process.exit(1);
64
+ }
65
+
66
+ generateModule(moduleName);
67
+ } else {
68
+ console.error(`❌ Unknown command: ${command}`);
69
+ console.log('Run with --help to see available commands');
70
+ process.exit(1);
71
+ }
72
+
73
+ function generateModule(name) {
74
+ // Helper functions
75
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
76
+ const kebabCase = (str) => str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
77
+ const pascalCase = (str) => str.split(/[-_]/).map(capitalize).join('');
78
+ const camelCase = (str) => {
79
+ const pascal = pascalCase(str);
80
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
81
+ };
82
+
83
+ const moduleNameLower = name.toLowerCase();
84
+ const moduleNamePascal = pascalCase(name);
85
+ const moduleNameCamel = camelCase(name);
86
+ const moduleNameKebab = kebabCase(name);
87
+
88
+ const cwd = process.cwd();
89
+ const srcPath = path.join(cwd, 'src');
90
+
91
+ // Check if src folder exists
92
+ if (!fs.existsSync(srcPath)) {
93
+ console.error('❌ Could not find src folder. Make sure you are in the root of your React project.');
94
+ process.exit(1);
95
+ }
96
+
97
+ // Templates
98
+ const listPageTemplate = `import { Badge } from '@kaushalparajuli/react-crud-ui';
99
+ import { CrudList } from '@/components/common';
100
+ import use${moduleNamePascal}Store from '@/stores/use${moduleNamePascal}Store';
101
+ import { formatDate } from '@/lib/utils';
102
+
103
+ // Column definitions (actions column is auto-added by DataTable)
104
+ const columns = [
105
+ {
106
+ key: 'name',
107
+ label: 'Name',
108
+ sortable: true,
109
+ },
110
+ {
111
+ key: 'created_at',
112
+ label: 'Created',
113
+ render: (value) => formatDate(value),
114
+ },
115
+ {
116
+ key: 'is_active',
117
+ label: 'Status',
118
+ render: (value) => (
119
+ <Badge variant={value ? 'success' : 'secondary'}>
120
+ {value ? 'Active' : 'Inactive'}
121
+ </Badge>
122
+ ),
123
+ },
124
+ ];
125
+
126
+ export default function ${moduleNamePascal}ListPage() {
127
+ const store = use${moduleNamePascal}Store();
128
+
129
+ return (
130
+ <CrudList
131
+ title="${moduleNamePascal}s"
132
+ description="Manage ${moduleNameLower}s"
133
+ createPath="/${moduleNameLower}s/create"
134
+ editPath="/${moduleNameLower}s"
135
+ store={store}
136
+ columns={columns}
137
+ searchPlaceholder="Search ${moduleNameLower}s..."
138
+ getDeleteItemName={(item) => item?.name}
139
+ />
140
+ );
141
+ }
142
+ `;
143
+
144
+ const formPageTemplate = `import { Box } from 'lucide-react';
145
+ import { CrudForm, QInput, QTextarea, QSwitch } from '@kaushalparajuli/react-crud-ui';
146
+ import use${moduleNamePascal}Store from '@/stores/use${moduleNamePascal}Store';
147
+
148
+ export default function ${moduleNamePascal}FormPage() {
149
+ const { form, setFormField } = use${moduleNamePascal}Store();
150
+
151
+ return (
152
+ <CrudForm
153
+ title="${moduleNamePascal}"
154
+ description="Enter the ${moduleNameLower} information below"
155
+ icon={Box}
156
+ listPath="/${moduleNameLower}s"
157
+ useStore={use${moduleNamePascal}Store}
158
+ >
159
+ <QInput
160
+ name="name"
161
+ label="Name"
162
+ value={form.values.name || ''}
163
+ onChange={(val) => setFormField('name', val)}
164
+ error={form.errors.name}
165
+ placeholder="Enter name"
166
+ required
167
+ />
168
+
169
+ <QTextarea
170
+ name="description"
171
+ label="Description"
172
+ value={form.values.description || ''}
173
+ onChange={(val) => setFormField('description', val)}
174
+ error={form.errors.description}
175
+ placeholder="Enter description"
176
+ rows={4}
177
+ />
178
+
179
+ <QSwitch
180
+ label="Active"
181
+ checked={form.values.is_active ?? true}
182
+ onChange={(val) => setFormField('is_active', val)}
183
+ />
184
+ </CrudForm>
185
+ );
186
+ }
187
+ `;
188
+
189
+ const detailPageTemplate = `import { Box } from 'lucide-react';
190
+ import { CrudDetail, Badge } from '@kaushalparajuli/react-crud-ui';
191
+ import use${moduleNamePascal}Store from '@/stores/use${moduleNamePascal}Store';
192
+ import { formatDate } from '@/lib/utils';
193
+
194
+ export default function ${moduleNamePascal}DetailPage() {
195
+ return (
196
+ <CrudDetail
197
+ title="${moduleNamePascal} Details"
198
+ description="View ${moduleNameLower} information"
199
+ icon={Box}
200
+ listPath="/${moduleNameLower}s"
201
+ editPath="/${moduleNameLower}s"
202
+ useStore={use${moduleNamePascal}Store}
203
+ getItemName={(item) => item?.name}
204
+ >
205
+ {(data) => (
206
+ <div className="grid gap-4 md:grid-cols-2">
207
+ <div className="space-y-2">
208
+ <p className="text-sm font-medium text-muted-foreground">Name</p>
209
+ <p className="text-sm">{data.name}</p>
210
+ </div>
211
+ <div className="space-y-2">
212
+ <p className="text-sm font-medium text-muted-foreground">Status</p>
213
+ <Badge variant={data.is_active ? 'success' : 'secondary'}>
214
+ {data.is_active ? 'Active' : 'Inactive'}
215
+ </Badge>
216
+ </div>
217
+ {data.description && (
218
+ <div className="space-y-2 md:col-span-2">
219
+ <p className="text-sm font-medium text-muted-foreground">Description</p>
220
+ <p className="text-sm">{data.description}</p>
221
+ </div>
222
+ )}
223
+ <div className="space-y-2">
224
+ <p className="text-sm font-medium text-muted-foreground">Created</p>
225
+ <p className="text-sm">{formatDate(data.created_at)}</p>
226
+ </div>
227
+ <div className="space-y-2">
228
+ <p className="text-sm font-medium text-muted-foreground">Updated</p>
229
+ <p className="text-sm">{formatDate(data.updated_at)}</p>
230
+ </div>
231
+ </div>
232
+ )}
233
+ </CrudDetail>
234
+ );
235
+ }
236
+ `;
237
+
238
+ const routesTemplate = `import ${moduleNamePascal}ListPage from '@/pages/${moduleNameLower}s/${moduleNamePascal}ListPage';
239
+ import ${moduleNamePascal}FormPage from '@/pages/${moduleNameLower}s/${moduleNamePascal}FormPage';
240
+ import ${moduleNamePascal}DetailPage from '@/pages/${moduleNameLower}s/${moduleNamePascal}DetailPage';
241
+
242
+ const ${moduleNameCamel}Routes = [
243
+ { path: '/${moduleNameLower}s', element: <${moduleNamePascal}ListPage /> },
244
+ { path: '/${moduleNameLower}s/create', element: <${moduleNamePascal}FormPage /> },
245
+ { path: '/${moduleNameLower}s/:id', element: <${moduleNamePascal}DetailPage /> },
246
+ { path: '/${moduleNameLower}s/:id/edit', element: <${moduleNamePascal}FormPage /> },
247
+ ];
248
+
249
+ export default ${moduleNameCamel}Routes;
250
+ `;
251
+
252
+ const storeTemplate = `import { createCrudStore } from './createCrudStore';
253
+
254
+ // Default form values for ${moduleNameLower}s
255
+ const defaultFormValues = {
256
+ name: '',
257
+ description: '',
258
+ is_active: true,
259
+ };
260
+
261
+ // Create CRUD store for ${moduleNameLower}s
262
+ const use${moduleNamePascal}Store = createCrudStore('${moduleNameLower}', '/${moduleNameLower}s/', defaultFormValues);
263
+
264
+ export default use${moduleNamePascal}Store;
265
+ `;
266
+
267
+ // Create directories
268
+ const pagesDir = path.join(srcPath, 'pages', `${moduleNameLower}s`);
269
+ const routesDir = path.join(srcPath, 'routes', 'modules');
270
+ const storesDir = path.join(srcPath, 'stores');
271
+
272
+ if (!fs.existsSync(pagesDir)) {
273
+ fs.mkdirSync(pagesDir, { recursive: true });
274
+ }
275
+
276
+ if (!fs.existsSync(routesDir)) {
277
+ fs.mkdirSync(routesDir, { recursive: true });
278
+ }
279
+
280
+ // Write files
281
+ const files = [
282
+ { path: path.join(pagesDir, `${moduleNamePascal}ListPage.jsx`), content: listPageTemplate },
283
+ { path: path.join(pagesDir, `${moduleNamePascal}FormPage.jsx`), content: formPageTemplate },
284
+ { path: path.join(pagesDir, `${moduleNamePascal}DetailPage.jsx`), content: detailPageTemplate },
285
+ { path: path.join(routesDir, `${moduleNameCamel}Routes.js`), content: routesTemplate },
286
+ { path: path.join(storesDir, `use${moduleNamePascal}Store.js`), content: storeTemplate },
287
+ ];
288
+
289
+ console.log(`\n🚀 Generating ${moduleNamePascal} module...\n`);
290
+
291
+ files.forEach(({ path: filePath, content }) => {
292
+ if (fs.existsSync(filePath)) {
293
+ console.log(`⚠️ File already exists: ${path.relative(cwd, filePath)}`);
294
+ } else {
295
+ fs.writeFileSync(filePath, content);
296
+ console.log(`✅ Created: ${path.relative(cwd, filePath)}`);
297
+ }
298
+ });
299
+
300
+ console.log(`
301
+ 📝 Next steps:
302
+
303
+ 1. Add route to src/routes/modules/index.js:
304
+ export { default as ${moduleNameCamel}Routes } from './${moduleNameCamel}Routes';
305
+
306
+ 2. Import in src/routes/index.jsx:
307
+ import { ..., ${moduleNameCamel}Routes } from './modules';
308
+
309
+ 3. Add to protected routes children:
310
+ ...${moduleNameCamel}Routes,
311
+
312
+ 4. Add sidebar menu item:
313
+ {
314
+ label: '${moduleNamePascal}s',
315
+ path: '/${moduleNameLower}s',
316
+ icon: Box,
317
+ },
318
+
319
+ 5. Customize the form fields in ${moduleNamePascal}FormPage.jsx
320
+
321
+ 6. Customize the table columns in ${moduleNamePascal}ListPage.jsx
322
+
323
+ 7. Update store default values in use${moduleNamePascal}Store.js
324
+
325
+ 8. Update detail view fields in ${moduleNamePascal}DetailPage.jsx
326
+ `);
327
+ }