@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 +327 -0
- package/dist/index.cjs +54 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3855 -3368
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -3
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
|
+
}
|