shadcn-nextjs-page-generator 1.0.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/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/index.cjs +1900 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1866 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
run: () => run
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_ora = __toESM(require("ora"), 1);
|
|
38
|
+
|
|
39
|
+
// src/cli/prompts.ts
|
|
40
|
+
var import_prompts = __toESM(require("prompts"), 1);
|
|
41
|
+
|
|
42
|
+
// src/utils/string-transforms.ts
|
|
43
|
+
function toPascalCase(str) {
|
|
44
|
+
return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
45
|
+
}
|
|
46
|
+
function toKebabCase(str) {
|
|
47
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/cli/validators.ts
|
|
51
|
+
function validateNotEmpty(value) {
|
|
52
|
+
return value.length > 0 || "This field is required";
|
|
53
|
+
}
|
|
54
|
+
function validateRoutePath(value) {
|
|
55
|
+
if (!value || value.length === 0) {
|
|
56
|
+
return "Route path is required";
|
|
57
|
+
}
|
|
58
|
+
if (!/^[a-z0-9\-\/]+$/.test(value)) {
|
|
59
|
+
return "Route path can only contain lowercase letters, numbers, dashes, and slashes";
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function validateColumnKey(value) {
|
|
64
|
+
if (!value || value.length === 0) {
|
|
65
|
+
return "Column key is required";
|
|
66
|
+
}
|
|
67
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value)) {
|
|
68
|
+
return "Column key must be a valid JavaScript identifier (camelCase recommended)";
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/cli/prompts.ts
|
|
74
|
+
async function collectConfiguration() {
|
|
75
|
+
import_prompts.default.override({ onCancel: () => process.exit(0) });
|
|
76
|
+
console.log("\n\u{1F680} Welcome to shadcn-page-gen!\n");
|
|
77
|
+
const { pageName } = await (0, import_prompts.default)({
|
|
78
|
+
type: "text",
|
|
79
|
+
name: "pageName",
|
|
80
|
+
message: 'What is the name of the page? (e.g., "User Management")',
|
|
81
|
+
validate: validateNotEmpty
|
|
82
|
+
});
|
|
83
|
+
if (!pageName) return null;
|
|
84
|
+
const defaultRoute = toKebabCase(pageName);
|
|
85
|
+
const { routePath } = await (0, import_prompts.default)({
|
|
86
|
+
type: "text",
|
|
87
|
+
name: "routePath",
|
|
88
|
+
message: `What is the route path? (e.g., "admin/users")`,
|
|
89
|
+
initial: defaultRoute,
|
|
90
|
+
validate: validateRoutePath
|
|
91
|
+
});
|
|
92
|
+
if (!routePath) return null;
|
|
93
|
+
const cleanRoutePath = routePath.replace(/^\/+|\/+$/g, "");
|
|
94
|
+
const defaultModuleName = cleanRoutePath.replace(/\//g, "-");
|
|
95
|
+
const { moduleName } = await (0, import_prompts.default)({
|
|
96
|
+
type: "text",
|
|
97
|
+
name: "moduleName",
|
|
98
|
+
message: `Module name?`,
|
|
99
|
+
initial: defaultModuleName
|
|
100
|
+
});
|
|
101
|
+
if (!moduleName) return null;
|
|
102
|
+
const { architecture } = await (0, import_prompts.default)({
|
|
103
|
+
type: "select",
|
|
104
|
+
name: "architecture",
|
|
105
|
+
message: "Choose architecture pattern:",
|
|
106
|
+
choices: [
|
|
107
|
+
{
|
|
108
|
+
title: "DDD (Domain-Driven Design)",
|
|
109
|
+
value: "ddd",
|
|
110
|
+
description: "Full layers: Domain, Application, Infrastructure, Presentation"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "Simplified",
|
|
114
|
+
value: "simplified",
|
|
115
|
+
description: "Just components and pages - cleaner, faster"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
initial: 0
|
|
119
|
+
});
|
|
120
|
+
if (!architecture) return null;
|
|
121
|
+
const pascalName = toPascalCase(pageName);
|
|
122
|
+
let entityName = pascalName;
|
|
123
|
+
if (architecture === "ddd") {
|
|
124
|
+
const response = await (0, import_prompts.default)({
|
|
125
|
+
type: "text",
|
|
126
|
+
name: "entityName",
|
|
127
|
+
message: `Entity name? (e.g., Product, Ticket)`,
|
|
128
|
+
initial: pascalName
|
|
129
|
+
});
|
|
130
|
+
if (!response.entityName) return null;
|
|
131
|
+
entityName = response.entityName;
|
|
132
|
+
}
|
|
133
|
+
const columns = [];
|
|
134
|
+
let addColumn = true;
|
|
135
|
+
console.log("\n--- Table Columns (ID is automatic) ---");
|
|
136
|
+
const { useDefaultColumns } = await (0, import_prompts.default)({
|
|
137
|
+
type: "confirm",
|
|
138
|
+
name: "useDefaultColumns",
|
|
139
|
+
message: "Use default columns? (Name, Status, Created At)",
|
|
140
|
+
initial: true
|
|
141
|
+
});
|
|
142
|
+
if (useDefaultColumns) {
|
|
143
|
+
columns.push(
|
|
144
|
+
{ label: "Name", key: "name", type: "string", sortable: true },
|
|
145
|
+
{ label: "Status", key: "status", type: "string", sortable: true },
|
|
146
|
+
{ label: "Created At", key: "createdAt", type: "date", sortable: true }
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
while (addColumn) {
|
|
150
|
+
const { continueAdding } = await (0, import_prompts.default)({
|
|
151
|
+
type: "confirm",
|
|
152
|
+
name: "continueAdding",
|
|
153
|
+
message: columns.length === 0 ? "Add a column?" : "Add another column?",
|
|
154
|
+
initial: true
|
|
155
|
+
});
|
|
156
|
+
if (!continueAdding) break;
|
|
157
|
+
const columnData = await (0, import_prompts.default)([
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
name: "label",
|
|
161
|
+
message: 'Column Label (e.g., "Email Address"):',
|
|
162
|
+
validate: validateNotEmpty
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
name: "key",
|
|
167
|
+
message: 'Column Key (e.g., "email"):',
|
|
168
|
+
validate: validateColumnKey
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "select",
|
|
172
|
+
name: "type",
|
|
173
|
+
message: "Column Type:",
|
|
174
|
+
choices: [
|
|
175
|
+
{ title: "String", value: "string" },
|
|
176
|
+
{ title: "Number", value: "number" },
|
|
177
|
+
{ title: "Boolean", value: "boolean" },
|
|
178
|
+
{ title: "Date", value: "date" }
|
|
179
|
+
],
|
|
180
|
+
initial: 0
|
|
181
|
+
}
|
|
182
|
+
]);
|
|
183
|
+
if (columnData.label && columnData.key && columnData.type) {
|
|
184
|
+
columns.push({
|
|
185
|
+
...columnData,
|
|
186
|
+
sortable: false
|
|
187
|
+
// Will be set later
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (columns.length === 0) {
|
|
192
|
+
columns.push(
|
|
193
|
+
{ label: "Name", key: "name", type: "string", sortable: true },
|
|
194
|
+
{ label: "Status", key: "status", type: "string", sortable: true },
|
|
195
|
+
{ label: "Created At", key: "createdAt", type: "date", sortable: true }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const filters = [];
|
|
200
|
+
console.log("\n--- Filters ---");
|
|
201
|
+
const { addFilters } = await (0, import_prompts.default)({
|
|
202
|
+
type: "confirm",
|
|
203
|
+
name: "addFilters",
|
|
204
|
+
message: "Add filters?",
|
|
205
|
+
initial: true
|
|
206
|
+
});
|
|
207
|
+
if (addFilters) {
|
|
208
|
+
let addFilterLoop = true;
|
|
209
|
+
while (addFilterLoop) {
|
|
210
|
+
const { continueAddingFilter } = await (0, import_prompts.default)({
|
|
211
|
+
type: "confirm",
|
|
212
|
+
name: "continueAddingFilter",
|
|
213
|
+
message: filters.length === 0 ? "Add a filter?" : "Add another filter?",
|
|
214
|
+
initial: filters.length === 0
|
|
215
|
+
});
|
|
216
|
+
if (!continueAddingFilter) break;
|
|
217
|
+
const filterData = await (0, import_prompts.default)([
|
|
218
|
+
{
|
|
219
|
+
type: "select",
|
|
220
|
+
name: "type",
|
|
221
|
+
message: "Filter type:",
|
|
222
|
+
choices: [
|
|
223
|
+
{ title: "Dropdown (Select)", value: "select" },
|
|
224
|
+
{ title: "Date Picker", value: "date" },
|
|
225
|
+
{ title: "Text Input", value: "input" }
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "text",
|
|
230
|
+
name: "label",
|
|
231
|
+
message: 'Filter Label (e.g., "Status"):',
|
|
232
|
+
validate: validateNotEmpty
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
name: "key",
|
|
237
|
+
message: 'Filter Key (URL param, e.g., "status"):',
|
|
238
|
+
validate: validateColumnKey
|
|
239
|
+
}
|
|
240
|
+
]);
|
|
241
|
+
if (filterData.type && filterData.label && filterData.key) {
|
|
242
|
+
filters.push(filterData);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log("\n--- UI Options ---");
|
|
247
|
+
const uiOptions = await (0, import_prompts.default)([
|
|
248
|
+
{
|
|
249
|
+
type: "confirm",
|
|
250
|
+
name: "includeStats",
|
|
251
|
+
message: "Include Stats Cards at the top?",
|
|
252
|
+
initial: true
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: "confirm",
|
|
256
|
+
name: "includeRowSelection",
|
|
257
|
+
message: "Include Row Selection (Checkboxes)?",
|
|
258
|
+
initial: false
|
|
259
|
+
}
|
|
260
|
+
]);
|
|
261
|
+
const { dataFetching } = await (0, import_prompts.default)({
|
|
262
|
+
type: "select",
|
|
263
|
+
name: "dataFetching",
|
|
264
|
+
message: "Data Fetching Strategy:",
|
|
265
|
+
choices: [
|
|
266
|
+
{ title: "Mock Data (Repository Pattern)", value: "mock", description: "Default mock data with repository pattern" },
|
|
267
|
+
{ title: "TanStack Query (React Query)", value: "tanstack", description: "Modern data fetching with caching" },
|
|
268
|
+
{ title: "Standard (fetch/useEffect)", value: "fetch", description: "Basic fetch with useEffect" }
|
|
269
|
+
],
|
|
270
|
+
initial: 0
|
|
271
|
+
});
|
|
272
|
+
const { enableSorting } = await (0, import_prompts.default)({
|
|
273
|
+
type: "confirm",
|
|
274
|
+
name: "enableSorting",
|
|
275
|
+
message: "Enable Column Sorting?",
|
|
276
|
+
initial: true
|
|
277
|
+
});
|
|
278
|
+
let sortableColumns = [];
|
|
279
|
+
if (enableSorting && columns.length > 0) {
|
|
280
|
+
const { selected } = await (0, import_prompts.default)({
|
|
281
|
+
type: "multiselect",
|
|
282
|
+
name: "selected",
|
|
283
|
+
message: "Select columns to enable sorting (Space to select, Enter to submit):",
|
|
284
|
+
choices: columns.map((c) => ({ title: c.label, value: c.key, selected: true })),
|
|
285
|
+
min: 0
|
|
286
|
+
});
|
|
287
|
+
sortableColumns = selected || [];
|
|
288
|
+
columns.forEach((col) => {
|
|
289
|
+
col.sortable = sortableColumns.includes(col.key);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
console.log("\n--- Animations (Framer Motion) ---");
|
|
293
|
+
const animationOptions = await (0, import_prompts.default)([
|
|
294
|
+
{
|
|
295
|
+
type: "confirm",
|
|
296
|
+
name: "pageTransitions",
|
|
297
|
+
message: "Add page transition animations?",
|
|
298
|
+
initial: true
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: "confirm",
|
|
302
|
+
name: "listAnimations",
|
|
303
|
+
message: "Animate table rows on load?",
|
|
304
|
+
initial: true
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: "confirm",
|
|
308
|
+
name: "cardAnimations",
|
|
309
|
+
message: "Animate stats cards?",
|
|
310
|
+
initial: uiOptions.includeStats
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
type: "select",
|
|
314
|
+
name: "intensity",
|
|
315
|
+
message: "Animation intensity:",
|
|
316
|
+
choices: [
|
|
317
|
+
{ title: "Subtle (professional)", value: "subtle" },
|
|
318
|
+
{ title: "Moderate (balanced)", value: "moderate" },
|
|
319
|
+
{ title: "Bold (eye-catching)", value: "bold" }
|
|
320
|
+
],
|
|
321
|
+
initial: 1
|
|
322
|
+
}
|
|
323
|
+
]);
|
|
324
|
+
const config = {
|
|
325
|
+
pageName,
|
|
326
|
+
routePath: cleanRoutePath,
|
|
327
|
+
moduleName,
|
|
328
|
+
architecture,
|
|
329
|
+
entityName,
|
|
330
|
+
columns,
|
|
331
|
+
filters,
|
|
332
|
+
includeStats: uiOptions.includeStats,
|
|
333
|
+
includeRowSelection: uiOptions.includeRowSelection,
|
|
334
|
+
includeSearch: true,
|
|
335
|
+
// Always include search
|
|
336
|
+
dataFetching,
|
|
337
|
+
sortableColumns,
|
|
338
|
+
animations: {
|
|
339
|
+
pageTransitions: animationOptions.pageTransitions,
|
|
340
|
+
listAnimations: animationOptions.listAnimations,
|
|
341
|
+
cardAnimations: animationOptions.cardAnimations,
|
|
342
|
+
intensity: animationOptions.intensity
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return config;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/utils/logger.ts
|
|
349
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
350
|
+
var logger = {
|
|
351
|
+
success(message) {
|
|
352
|
+
console.log(import_chalk.default.green("\u2713"), message);
|
|
353
|
+
},
|
|
354
|
+
error(message) {
|
|
355
|
+
console.log(import_chalk.default.red("\u2717"), message);
|
|
356
|
+
},
|
|
357
|
+
info(message) {
|
|
358
|
+
console.log(import_chalk.default.blue("\u2139"), message);
|
|
359
|
+
},
|
|
360
|
+
warning(message) {
|
|
361
|
+
console.log(import_chalk.default.yellow("\u26A0"), message);
|
|
362
|
+
},
|
|
363
|
+
log(message) {
|
|
364
|
+
console.log(message);
|
|
365
|
+
},
|
|
366
|
+
title(message) {
|
|
367
|
+
console.log(import_chalk.default.bold.cyan(`
|
|
368
|
+
${message}
|
|
369
|
+
`));
|
|
370
|
+
},
|
|
371
|
+
step(step, total, message) {
|
|
372
|
+
console.log(import_chalk.default.gray(`[${step}/${total}]`), message);
|
|
373
|
+
},
|
|
374
|
+
dim(message) {
|
|
375
|
+
console.log(import_chalk.default.dim(message));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/generators/ddd-generator.ts
|
|
380
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
381
|
+
|
|
382
|
+
// src/utils/file-system.ts
|
|
383
|
+
var import_fs_extra = __toESM(require("fs-extra"), 1);
|
|
384
|
+
var import_path = __toESM(require("path"), 1);
|
|
385
|
+
async function ensureDir(dirPath) {
|
|
386
|
+
await import_fs_extra.default.ensureDir(dirPath);
|
|
387
|
+
}
|
|
388
|
+
async function writeFile(filePath, content) {
|
|
389
|
+
const dir = import_path.default.dirname(filePath);
|
|
390
|
+
await ensureDir(dir);
|
|
391
|
+
await import_fs_extra.default.writeFile(filePath, content, "utf-8");
|
|
392
|
+
}
|
|
393
|
+
async function createDirectories(dirs) {
|
|
394
|
+
for (const dir of dirs) {
|
|
395
|
+
await ensureDir(dir);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function writeFiles(files) {
|
|
399
|
+
for (const file of files) {
|
|
400
|
+
await writeFile(file.path, file.content);
|
|
401
|
+
logger.dim(` Created: ${file.path}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/templates/ddd/entity.ts
|
|
406
|
+
function generateEntity(config) {
|
|
407
|
+
const { entityName, moduleName, columns } = config;
|
|
408
|
+
const columnFields = columns.map((c) => {
|
|
409
|
+
const typeMap = {
|
|
410
|
+
"string": "string",
|
|
411
|
+
"number": "number",
|
|
412
|
+
"boolean": "boolean",
|
|
413
|
+
"date": "string"
|
|
414
|
+
// ISO string for dates
|
|
415
|
+
};
|
|
416
|
+
return ` ${c.key}: ${typeMap[c.type]};`;
|
|
417
|
+
}).join("\n");
|
|
418
|
+
const createDTOFields = columns.filter((c) => !["id", "createdAt", "updatedAt"].includes(c.key)).map((c) => {
|
|
419
|
+
const typeMap = {
|
|
420
|
+
"string": "string",
|
|
421
|
+
"number": "number",
|
|
422
|
+
"boolean": "boolean",
|
|
423
|
+
"date": "string"
|
|
424
|
+
};
|
|
425
|
+
return ` ${c.key}: ${typeMap[c.type]};`;
|
|
426
|
+
}).join("\n");
|
|
427
|
+
return `/**
|
|
428
|
+
* Domain Entity: ${entityName}
|
|
429
|
+
* Generated by shadcn-page-gen
|
|
430
|
+
*/
|
|
431
|
+
export interface ${entityName} {
|
|
432
|
+
id: string;
|
|
433
|
+
${columnFields}
|
|
434
|
+
updatedAt: Date;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export interface Create${entityName}DTO {
|
|
438
|
+
${createDTOFields || " // Add your fields here"}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface Update${entityName}DTO extends Partial<Create${entityName}DTO> {
|
|
442
|
+
id: string;
|
|
443
|
+
}
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/templates/ddd/repository-interface.ts
|
|
448
|
+
function generateRepositoryInterface(config) {
|
|
449
|
+
const { entityName, moduleName } = config;
|
|
450
|
+
return `import { ${entityName}, Create${entityName}DTO, Update${entityName}DTO } from '../entities/${moduleName}.entity';
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Repository Interface: ${entityName}
|
|
454
|
+
* Generated by shadcn-page-gen
|
|
455
|
+
*/
|
|
456
|
+
export interface I${entityName}Repository {
|
|
457
|
+
findAll(params?: any): Promise<${entityName}[]>;
|
|
458
|
+
findById(id: string): Promise<${entityName} | null>;
|
|
459
|
+
create(data: Create${entityName}DTO): Promise<${entityName}>;
|
|
460
|
+
update(data: Update${entityName}DTO): Promise<${entityName}>;
|
|
461
|
+
delete(id: string): Promise<void>;
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/templates/ddd/repository-impl.ts
|
|
467
|
+
function generateRepositoryImpl(config) {
|
|
468
|
+
const { entityName, moduleName, columns } = config;
|
|
469
|
+
const mockFields = columns.map((c) => {
|
|
470
|
+
if (c.key === "createdAt") {
|
|
471
|
+
return ` createdAt: new Date().toISOString(),`;
|
|
472
|
+
}
|
|
473
|
+
if (c.key === "status") {
|
|
474
|
+
return ` status: Math.random() > 0.5 ? 'Active' : 'Inactive',`;
|
|
475
|
+
}
|
|
476
|
+
if (c.key === "name") {
|
|
477
|
+
return ` name: \`${entityName} \${i + 1}\`,`;
|
|
478
|
+
}
|
|
479
|
+
if (c.type === "number") {
|
|
480
|
+
return ` ${c.key}: (i + 1) * 100,`;
|
|
481
|
+
}
|
|
482
|
+
if (c.type === "boolean") {
|
|
483
|
+
return ` ${c.key}: Math.random() > 0.5,`;
|
|
484
|
+
}
|
|
485
|
+
if (c.type === "date") {
|
|
486
|
+
return ` ${c.key}: new Date().toISOString(),`;
|
|
487
|
+
}
|
|
488
|
+
return ` ${c.key}: \`${c.label} \${i + 1}\`,`;
|
|
489
|
+
}).join("\n");
|
|
490
|
+
return `import { ${entityName}, Create${entityName}DTO, Update${entityName}DTO } from '../../domain/entities/${moduleName}.entity';
|
|
491
|
+
import { I${entityName}Repository } from '../../domain/repositories/${moduleName}.repository.interface';
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Repository Implementation: ${entityName}
|
|
495
|
+
* Generated by shadcn-page-gen
|
|
496
|
+
*
|
|
497
|
+
* This is a mock implementation with in-memory data.
|
|
498
|
+
* Replace with your actual API calls.
|
|
499
|
+
*/
|
|
500
|
+
export class ${entityName}Repository implements I${entityName}Repository {
|
|
501
|
+
// Mock data - replace with real API calls
|
|
502
|
+
private items: ${entityName}[] = Array.from({ length: 10 }).map((_, i) => ({
|
|
503
|
+
id: (i + 1).toString(),
|
|
504
|
+
${mockFields}
|
|
505
|
+
updatedAt: new Date(),
|
|
506
|
+
})) as unknown as ${entityName}[];
|
|
507
|
+
|
|
508
|
+
async findAll(params?: any): Promise<${entityName}[]> {
|
|
509
|
+
// Simulate API delay
|
|
510
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
511
|
+
|
|
512
|
+
// TODO: Implement sorting, filtering, pagination
|
|
513
|
+
// const { sortBy, order, page, limit } = params || {};
|
|
514
|
+
|
|
515
|
+
return [...this.items];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async findById(id: string): Promise<${entityName} | null> {
|
|
519
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
520
|
+
const item = this.items.find(i => i.id === id);
|
|
521
|
+
return item || null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async create(data: Create${entityName}DTO): Promise<${entityName}> {
|
|
525
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
526
|
+
|
|
527
|
+
const newItem: ${entityName} = {
|
|
528
|
+
id: Date.now().toString(),
|
|
529
|
+
...data,
|
|
530
|
+
updatedAt: new Date(),
|
|
531
|
+
${columns.some((c) => c.key === "createdAt") ? "createdAt: new Date().toISOString()," : ""}
|
|
532
|
+
} as unknown as ${entityName};
|
|
533
|
+
|
|
534
|
+
this.items.push(newItem);
|
|
535
|
+
return newItem;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async update(data: Update${entityName}DTO): Promise<${entityName}> {
|
|
539
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
540
|
+
|
|
541
|
+
const index = this.items.findIndex(i => i.id === data.id);
|
|
542
|
+
if (index === -1) {
|
|
543
|
+
throw new Error(\`${entityName} with id \${data.id} not found\`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this.items[index] = {
|
|
547
|
+
...this.items[index],
|
|
548
|
+
...data,
|
|
549
|
+
updatedAt: new Date(),
|
|
550
|
+
} as ${entityName};
|
|
551
|
+
|
|
552
|
+
return this.items[index];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async delete(id: string): Promise<void> {
|
|
556
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
557
|
+
|
|
558
|
+
const index = this.items.findIndex(i => i.id === id);
|
|
559
|
+
if (index === -1) {
|
|
560
|
+
throw new Error(\`${entityName} with id \${id} not found\`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.items.splice(index, 1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/templates/ddd/use-case.ts
|
|
570
|
+
function generateUseCase(config) {
|
|
571
|
+
const { entityName, moduleName } = config;
|
|
572
|
+
return `import { ${entityName} } from '../../domain/entities/${moduleName}.entity';
|
|
573
|
+
import { I${entityName}Repository } from '../../domain/repositories/${moduleName}.repository.interface';
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Use Case: Get ${entityName}s
|
|
577
|
+
* Generated by shadcn-page-gen
|
|
578
|
+
*/
|
|
579
|
+
export class Get${entityName}sUseCase {
|
|
580
|
+
constructor(private readonly repository: I${entityName}Repository) {}
|
|
581
|
+
|
|
582
|
+
async execute(params?: any): Promise<${entityName}[]> {
|
|
583
|
+
return await this.repository.findAll(params);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/templates/ddd/component.ts
|
|
590
|
+
function generateComponent(config) {
|
|
591
|
+
const {
|
|
592
|
+
entityName,
|
|
593
|
+
moduleName,
|
|
594
|
+
pageName,
|
|
595
|
+
routePath,
|
|
596
|
+
columns,
|
|
597
|
+
filters,
|
|
598
|
+
includeStats,
|
|
599
|
+
includeRowSelection,
|
|
600
|
+
dataFetching,
|
|
601
|
+
sortableColumns,
|
|
602
|
+
animations
|
|
603
|
+
} = config;
|
|
604
|
+
const isTanStack = dataFetching === "tanstack";
|
|
605
|
+
const hasAnimations = animations.listAnimations || animations.cardAnimations;
|
|
606
|
+
const imports = `'use client';
|
|
607
|
+
|
|
608
|
+
${hasAnimations ? `import { motion } from 'framer-motion';` : ""}
|
|
609
|
+
import { useEffect, useState } from 'react';
|
|
610
|
+
import { ${entityName} } from '../../domain/entities/${moduleName}.entity';
|
|
611
|
+
import { Get${entityName}sUseCase } from '../../application/use-cases/get-${moduleName}s.use-case';
|
|
612
|
+
import { ${entityName}Repository } from '../../infrastructure/repositories/${moduleName}.repository';
|
|
613
|
+
${isTanStack ? `import { useQuery } from '@tanstack/react-query';` : ""}
|
|
614
|
+
import { Button } from '@/components/ui/button';
|
|
615
|
+
import {
|
|
616
|
+
Table,
|
|
617
|
+
TableBody,
|
|
618
|
+
TableCell,
|
|
619
|
+
TableHead,
|
|
620
|
+
TableHeader,
|
|
621
|
+
TableRow,
|
|
622
|
+
} from '@/components/ui/table';
|
|
623
|
+
import {
|
|
624
|
+
Select,
|
|
625
|
+
SelectContent,
|
|
626
|
+
SelectItem,
|
|
627
|
+
SelectTrigger,
|
|
628
|
+
SelectValue,
|
|
629
|
+
} from '@/components/ui/select';
|
|
630
|
+
import { Input } from '@/components/ui/input';
|
|
631
|
+
${filters.some((f) => f.type === "date") ? `import { Calendar } from '@/components/ui/calendar';
|
|
632
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
633
|
+
import { Calendar as CalendarIcon } from 'lucide-react';
|
|
634
|
+
import { format } from 'date-fns';` : ""}
|
|
635
|
+
import { Badge } from '@/components/ui/badge';
|
|
636
|
+
${includeStats ? `import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';` : ""}
|
|
637
|
+
import {
|
|
638
|
+
Search,
|
|
639
|
+
Plus,
|
|
640
|
+
X,
|
|
641
|
+
RefreshCcw,
|
|
642
|
+
Eye,
|
|
643
|
+
Pencil,
|
|
644
|
+
Trash2,
|
|
645
|
+
MoreHorizontal,
|
|
646
|
+
${sortableColumns.length > 0 ? "ArrowUpDown, ArrowUp, ArrowDown," : ""}
|
|
647
|
+
} from 'lucide-react';
|
|
648
|
+
import { cn } from '@/lib/utils';
|
|
649
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
650
|
+
import {
|
|
651
|
+
Pagination,
|
|
652
|
+
PaginationContent,
|
|
653
|
+
PaginationItem,
|
|
654
|
+
PaginationLink,
|
|
655
|
+
PaginationNext,
|
|
656
|
+
PaginationPrevious,
|
|
657
|
+
} from "@/components/ui/pagination";
|
|
658
|
+
import {
|
|
659
|
+
DropdownMenu,
|
|
660
|
+
DropdownMenuContent,
|
|
661
|
+
DropdownMenuItem,
|
|
662
|
+
DropdownMenuLabel,
|
|
663
|
+
DropdownMenuTrigger,
|
|
664
|
+
} from "@/components/ui/dropdown-menu";
|
|
665
|
+
${includeRowSelection ? `import { Checkbox } from "@/components/ui/checkbox";` : ""}`;
|
|
666
|
+
const animationVariants = hasAnimations ? `
|
|
667
|
+
// Framer Motion animation variants
|
|
668
|
+
const containerVariants = {
|
|
669
|
+
hidden: { opacity: 0 },
|
|
670
|
+
visible: {
|
|
671
|
+
opacity: 1,
|
|
672
|
+
transition: {
|
|
673
|
+
staggerChildren: ${animations.intensity === "bold" ? "0.1" : animations.intensity === "subtle" ? "0.03" : "0.05"}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const itemVariants = {
|
|
679
|
+
hidden: { opacity: 0, x: -20 },
|
|
680
|
+
visible: {
|
|
681
|
+
opacity: 1,
|
|
682
|
+
x: 0,
|
|
683
|
+
transition: { duration: ${animations.intensity === "bold" ? "0.3" : animations.intensity === "subtle" ? "0.15" : "0.2"} }
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
` : "";
|
|
687
|
+
const dataFetchingCode = isTanStack ? `
|
|
688
|
+
// TanStack Query data fetching
|
|
689
|
+
const { data, isLoading: loading, refetch } = useQuery({
|
|
690
|
+
queryKey: ['${moduleName}', searchParams.toString()],
|
|
691
|
+
queryFn: async () => {
|
|
692
|
+
const repo = new ${entityName}Repository();
|
|
693
|
+
const useCase = new Get${entityName}sUseCase(repo);
|
|
694
|
+
return await useCase.execute({
|
|
695
|
+
q: searchParams.get('q'),
|
|
696
|
+
page: searchParams.get('page'),
|
|
697
|
+
limit: pageSize,
|
|
698
|
+
sortBy: searchParams.get('sortBy'),
|
|
699
|
+
order: searchParams.get('order'),
|
|
700
|
+
...Object.fromEntries(searchParams.entries())
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
});` : `
|
|
704
|
+
const [data, setData] = useState<${entityName}[]>([]);
|
|
705
|
+
const [loading, setLoading] = useState(true);
|
|
706
|
+
|
|
707
|
+
const fetchData = async () => {
|
|
708
|
+
setLoading(true);
|
|
709
|
+
try {
|
|
710
|
+
const repo = new ${entityName}Repository();
|
|
711
|
+
const useCase = new Get${entityName}sUseCase(repo);
|
|
712
|
+
const result = await useCase.execute({
|
|
713
|
+
q: searchParams.get('q'),
|
|
714
|
+
page: searchParams.get('page'),
|
|
715
|
+
limit: pageSize,
|
|
716
|
+
sortBy: searchParams.get('sortBy'),
|
|
717
|
+
order: searchParams.get('order'),
|
|
718
|
+
...Object.fromEntries(searchParams.entries())
|
|
719
|
+
});
|
|
720
|
+
setData(result);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error(error);
|
|
723
|
+
} finally {
|
|
724
|
+
setLoading(false);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
useEffect(() => {
|
|
729
|
+
fetchData();
|
|
730
|
+
}, [searchParams]);`;
|
|
731
|
+
const rowSelectionCode = includeRowSelection ? `
|
|
732
|
+
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
|
733
|
+
|
|
734
|
+
const toggleRow = (id: string) => {
|
|
735
|
+
const newSelected = new Set(selectedRows);
|
|
736
|
+
if (newSelected.has(id)) newSelected.delete(id);
|
|
737
|
+
else newSelected.add(id);
|
|
738
|
+
setSelectedRows(newSelected);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const toggleAll = () => {
|
|
742
|
+
if (selectedRows.size === (data?.length || 0)) {
|
|
743
|
+
setSelectedRows(new Set());
|
|
744
|
+
} else {
|
|
745
|
+
setSelectedRows(new Set(data?.map(i => i.id) || []));
|
|
746
|
+
}
|
|
747
|
+
};` : "";
|
|
748
|
+
const statsCards = includeStats ? `
|
|
749
|
+
{/* Stats Cards */}
|
|
750
|
+
${animations.cardAnimations ? `<motion.div
|
|
751
|
+
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
|
752
|
+
variants={containerVariants}
|
|
753
|
+
initial="hidden"
|
|
754
|
+
animate="visible"
|
|
755
|
+
>` : `<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">`}
|
|
756
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
757
|
+
<Card>
|
|
758
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
759
|
+
<CardTitle className="text-sm font-medium">Total ${entityName}s</CardTitle>
|
|
760
|
+
</CardHeader>
|
|
761
|
+
<CardContent>
|
|
762
|
+
<div className="text-2xl font-bold">128</div>
|
|
763
|
+
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
|
|
764
|
+
</CardContent>
|
|
765
|
+
</Card>
|
|
766
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
767
|
+
|
|
768
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
769
|
+
<Card>
|
|
770
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
771
|
+
<CardTitle className="text-sm font-medium">Active</CardTitle>
|
|
772
|
+
</CardHeader>
|
|
773
|
+
<CardContent>
|
|
774
|
+
<div className="text-2xl font-bold">12</div>
|
|
775
|
+
<p className="text-xs text-muted-foreground">+4 since yesterday</p>
|
|
776
|
+
</CardContent>
|
|
777
|
+
</Card>
|
|
778
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
779
|
+
|
|
780
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
781
|
+
<Card>
|
|
782
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
783
|
+
<CardTitle className="text-sm font-medium">Pending</CardTitle>
|
|
784
|
+
</CardHeader>
|
|
785
|
+
<CardContent>
|
|
786
|
+
<div className="text-2xl font-bold">4</div>
|
|
787
|
+
<p className="text-xs text-muted-foreground">-2 since yesterday</p>
|
|
788
|
+
</CardContent>
|
|
789
|
+
</Card>
|
|
790
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
791
|
+
|
|
792
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
793
|
+
<Card>
|
|
794
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
795
|
+
<CardTitle className="text-sm font-medium">Closed</CardTitle>
|
|
796
|
+
</CardHeader>
|
|
797
|
+
<CardContent>
|
|
798
|
+
<div className="text-2xl font-bold">2</div>
|
|
799
|
+
<p className="text-xs text-muted-foreground">+1 since yesterday</p>
|
|
800
|
+
</CardContent>
|
|
801
|
+
</Card>
|
|
802
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
803
|
+
${animations.cardAnimations ? `</motion.div>` : `</div>`}
|
|
804
|
+
` : "";
|
|
805
|
+
const filterComponents = filters.map((f) => {
|
|
806
|
+
if (f.type === "select") {
|
|
807
|
+
return `
|
|
808
|
+
<Select
|
|
809
|
+
value={searchParams.get('${f.key}') || 'all'}
|
|
810
|
+
onValueChange={(value) => updateParam('${f.key}', value === 'all' ? null : value)}
|
|
811
|
+
>
|
|
812
|
+
<SelectTrigger className="w-[150px]">
|
|
813
|
+
<SelectValue placeholder="${f.label}" />
|
|
814
|
+
</SelectTrigger>
|
|
815
|
+
<SelectContent>
|
|
816
|
+
<SelectItem value="all">All ${f.label}</SelectItem>
|
|
817
|
+
<SelectItem value="active">Active</SelectItem>
|
|
818
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
819
|
+
</SelectContent>
|
|
820
|
+
</Select>`;
|
|
821
|
+
} else if (f.type === "date") {
|
|
822
|
+
return `
|
|
823
|
+
<Popover>
|
|
824
|
+
<PopoverTrigger asChild>
|
|
825
|
+
<Button
|
|
826
|
+
variant="outline"
|
|
827
|
+
className={cn(
|
|
828
|
+
"w-[200px] justify-start text-left font-normal",
|
|
829
|
+
!searchParams.get('${f.key}') && "text-muted-foreground"
|
|
830
|
+
)}
|
|
831
|
+
>
|
|
832
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
833
|
+
{searchParams.get('${f.key}') ? (
|
|
834
|
+
format(new Date(searchParams.get('${f.key}')!), "PPP")
|
|
835
|
+
) : (
|
|
836
|
+
<span>${f.label}</span>
|
|
837
|
+
)}
|
|
838
|
+
</Button>
|
|
839
|
+
</PopoverTrigger>
|
|
840
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
841
|
+
<Calendar
|
|
842
|
+
mode="single"
|
|
843
|
+
selected={searchParams.get('${f.key}') ? new Date(searchParams.get('${f.key}')!) : undefined}
|
|
844
|
+
onSelect={(date) => updateParam('${f.key}', date ? date.toISOString() : null)}
|
|
845
|
+
initialFocus
|
|
846
|
+
/>
|
|
847
|
+
</PopoverContent>
|
|
848
|
+
</Popover>`;
|
|
849
|
+
}
|
|
850
|
+
return "";
|
|
851
|
+
}).join("\n");
|
|
852
|
+
const tableHeaders = columns.map((c) => {
|
|
853
|
+
const isSortable = sortableColumns.includes(c.key);
|
|
854
|
+
if (isSortable) {
|
|
855
|
+
return ` <TableHead className="cursor-pointer" onClick={() => handleSort('${c.key}')}>
|
|
856
|
+
<div className="flex items-center gap-1">
|
|
857
|
+
${c.label.toUpperCase()}
|
|
858
|
+
{searchParams.get('sortBy') === '${c.key}' ? (
|
|
859
|
+
searchParams.get('order') === 'asc' ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />
|
|
860
|
+
) : <ArrowUpDown className="h-3 w-3 text-muted-foreground" />}
|
|
861
|
+
</div>
|
|
862
|
+
</TableHead>`;
|
|
863
|
+
}
|
|
864
|
+
return ` <TableHead>${c.label.toUpperCase()}</TableHead>`;
|
|
865
|
+
}).join("\n");
|
|
866
|
+
const tableCells = columns.map((c) => {
|
|
867
|
+
if (c.key === "status") {
|
|
868
|
+
return ` <TableCell>
|
|
869
|
+
<Badge variant={item.status === 'Active' ? 'default' : 'secondary'} className="rounded-full">
|
|
870
|
+
{item.status}
|
|
871
|
+
</Badge>
|
|
872
|
+
</TableCell>`;
|
|
873
|
+
}
|
|
874
|
+
if (c.type === "date") {
|
|
875
|
+
return ` <TableCell className="text-muted-foreground">{new Date(item.${c.key}).toLocaleDateString()}</TableCell>`;
|
|
876
|
+
}
|
|
877
|
+
return ` <TableCell>{item.${c.key}}</TableCell>`;
|
|
878
|
+
}).join("\n");
|
|
879
|
+
const colSpan = columns.length + 2 + (includeRowSelection ? 1 : 0);
|
|
880
|
+
return `${imports}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* ${entityName} List Component
|
|
884
|
+
* Generated by shadcn-page-gen
|
|
885
|
+
*
|
|
886
|
+
* Features:
|
|
887
|
+
* - Search and filters
|
|
888
|
+
* - Sorting: ${sortableColumns.length > 0 ? sortableColumns.join(", ") : "None"}
|
|
889
|
+
* - Pagination
|
|
890
|
+
* - Row actions (View, Edit, Delete)
|
|
891
|
+
* ${includeStats ? "- Stats cards" : ""}
|
|
892
|
+
* ${includeRowSelection ? "- Row selection" : ""}
|
|
893
|
+
* ${animations.listAnimations || animations.cardAnimations ? `- Framer Motion animations (${animations.intensity})` : ""}
|
|
894
|
+
*/
|
|
895
|
+
${animationVariants}
|
|
896
|
+
export function ${entityName}List() {
|
|
897
|
+
const router = useRouter();
|
|
898
|
+
const searchParams = useSearchParams();
|
|
899
|
+
|
|
900
|
+
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '');
|
|
901
|
+
const pageSize = Number(searchParams.get('limit')) || 10;
|
|
902
|
+
${dataFetchingCode}
|
|
903
|
+
${rowSelectionCode}
|
|
904
|
+
|
|
905
|
+
const updateParam = (key: string, value: string | null) => {
|
|
906
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
907
|
+
if (value) params.set(key, value);
|
|
908
|
+
else params.delete(key);
|
|
909
|
+
|
|
910
|
+
if (key !== 'page') params.set('page', '1');
|
|
911
|
+
|
|
912
|
+
router.push('?' + params.toString());
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
${sortableColumns.length > 0 ? `const handleSort = (key: string) => {
|
|
916
|
+
const currentSort = searchParams.get('sortBy');
|
|
917
|
+
const currentOrder = searchParams.get('order');
|
|
918
|
+
|
|
919
|
+
let newOrder = 'asc';
|
|
920
|
+
if (currentSort === key && currentOrder === 'asc') {
|
|
921
|
+
newOrder = 'desc';
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
925
|
+
params.set('sortBy', key);
|
|
926
|
+
params.set('order', newOrder);
|
|
927
|
+
router.push('?' + params.toString());
|
|
928
|
+
};` : ""}
|
|
929
|
+
|
|
930
|
+
return (
|
|
931
|
+
<div className="space-y-6">
|
|
932
|
+
${statsCards}
|
|
933
|
+
{/* Actions Bar */}
|
|
934
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
935
|
+
<div className="flex flex-1 items-center gap-2">
|
|
936
|
+
<div className="relative flex-1">
|
|
937
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
938
|
+
<Input
|
|
939
|
+
placeholder="Search..."
|
|
940
|
+
value={searchTerm}
|
|
941
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
942
|
+
onBlur={() => updateParam('q', searchTerm)}
|
|
943
|
+
className="pl-9 w-full md:max-w-md"
|
|
944
|
+
/>
|
|
945
|
+
</div>
|
|
946
|
+
${filterComponents}
|
|
947
|
+
{(searchParams.toString().length > 0) && (
|
|
948
|
+
<Button variant="ghost" size="icon" onClick={() => router.push('/${routePath}')} title="Reset Filters">
|
|
949
|
+
<X className="h-4 w-4" />
|
|
950
|
+
</Button>
|
|
951
|
+
)}
|
|
952
|
+
</div>
|
|
953
|
+
|
|
954
|
+
<div className="flex items-center gap-2">
|
|
955
|
+
<Button variant="outline" size="sm" onClick={() => ${isTanStack ? "refetch()" : "fetchData()"}}>
|
|
956
|
+
<RefreshCcw className="mr-2 h-4 w-4" /> Refresh
|
|
957
|
+
</Button>
|
|
958
|
+
<Button size="sm">
|
|
959
|
+
<Plus className="mr-2 h-4 w-4" /> New ${entityName}
|
|
960
|
+
</Button>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
{/* Main Table */}
|
|
965
|
+
<Card>
|
|
966
|
+
<CardContent className="p-0">
|
|
967
|
+
<Table>
|
|
968
|
+
<TableHeader>
|
|
969
|
+
<TableRow>
|
|
970
|
+
${includeRowSelection ? `<TableHead className="w-[50px]"><Checkbox checked={data?.length > 0 && selectedRows.size === data?.length} onCheckedChange={toggleAll} /></TableHead>` : ""}
|
|
971
|
+
<TableHead className="w-[100px]">ID</TableHead>
|
|
972
|
+
${tableHeaders}
|
|
973
|
+
<TableHead className="text-right">ACTIONS</TableHead>
|
|
974
|
+
</TableRow>
|
|
975
|
+
</TableHeader>
|
|
976
|
+
<TableBody>
|
|
977
|
+
{loading ? (
|
|
978
|
+
<TableRow>
|
|
979
|
+
<TableCell colSpan={${colSpan}} className="text-center h-24 text-muted-foreground">Loading data...</TableCell>
|
|
980
|
+
</TableRow>
|
|
981
|
+
) : !data || data.length === 0 ? (
|
|
982
|
+
<TableRow>
|
|
983
|
+
<TableCell colSpan={${colSpan}} className="text-center h-24 text-muted-foreground">No results found.</TableCell>
|
|
984
|
+
</TableRow>
|
|
985
|
+
) : (
|
|
986
|
+
${animations.listAnimations ? `
|
|
987
|
+
data.map((item, index) => (
|
|
988
|
+
<motion.tr
|
|
989
|
+
key={item.id}
|
|
990
|
+
variants={itemVariants}
|
|
991
|
+
initial="hidden"
|
|
992
|
+
animate="visible"
|
|
993
|
+
custom={index}
|
|
994
|
+
className="group"
|
|
995
|
+
>` : `data.map((item) => (
|
|
996
|
+
<TableRow key={item.id} className="group">`}
|
|
997
|
+
${includeRowSelection ? `<TableCell><Checkbox checked={selectedRows.has(item.id)} onCheckedChange={() => toggleRow(item.id)} /></TableCell>` : ""}
|
|
998
|
+
<TableCell className="font-medium">{item.id}</TableCell>
|
|
999
|
+
${tableCells}
|
|
1000
|
+
<TableCell className="text-right">
|
|
1001
|
+
<div className="flex justify-end gap-2">
|
|
1002
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950">
|
|
1003
|
+
<Eye className="h-4 w-4" />
|
|
1004
|
+
</Button>
|
|
1005
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-green-500 hover:text-green-600 hover:bg-green-50 dark:hover:bg-green-950">
|
|
1006
|
+
<Pencil className="h-4 w-4" />
|
|
1007
|
+
</Button>
|
|
1008
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950">
|
|
1009
|
+
<Trash2 className="h-4 w-4" />
|
|
1010
|
+
</Button>
|
|
1011
|
+
<DropdownMenu>
|
|
1012
|
+
<DropdownMenuTrigger asChild>
|
|
1013
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
1014
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
1015
|
+
</Button>
|
|
1016
|
+
</DropdownMenuTrigger>
|
|
1017
|
+
<DropdownMenuContent align="end">
|
|
1018
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
1019
|
+
<DropdownMenuItem>View Details</DropdownMenuItem>
|
|
1020
|
+
<DropdownMenuItem>Edit Record</DropdownMenuItem>
|
|
1021
|
+
</DropdownMenuContent>
|
|
1022
|
+
</DropdownMenu>
|
|
1023
|
+
</div>
|
|
1024
|
+
</TableCell>
|
|
1025
|
+
${animations.listAnimations ? `</motion.tr>` : `</TableRow>`}
|
|
1026
|
+
))
|
|
1027
|
+
)}
|
|
1028
|
+
</TableBody>
|
|
1029
|
+
</Table>
|
|
1030
|
+
</CardContent>
|
|
1031
|
+
</Card>
|
|
1032
|
+
|
|
1033
|
+
{/* Pagination */}
|
|
1034
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
1035
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
1036
|
+
<span>Show</span>
|
|
1037
|
+
<Select value={pageSize.toString()} onValueChange={(v) => updateParam('limit', v)}>
|
|
1038
|
+
<SelectTrigger className="h-8 w-[70px]">
|
|
1039
|
+
<SelectValue placeholder={pageSize.toString()} />
|
|
1040
|
+
</SelectTrigger>
|
|
1041
|
+
<SelectContent side="top">
|
|
1042
|
+
{[10, 20, 30, 50, 100].map((size) => (
|
|
1043
|
+
<SelectItem key={size} value={size.toString()}>
|
|
1044
|
+
{size}
|
|
1045
|
+
</SelectItem>
|
|
1046
|
+
))}
|
|
1047
|
+
</SelectContent>
|
|
1048
|
+
</Select>
|
|
1049
|
+
<span>entries</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
<Pagination className="justify-end w-auto">
|
|
1052
|
+
<PaginationContent>
|
|
1053
|
+
<PaginationItem>
|
|
1054
|
+
<PaginationPrevious href="#" />
|
|
1055
|
+
</PaginationItem>
|
|
1056
|
+
<PaginationItem>
|
|
1057
|
+
<PaginationLink href="#" isActive>1</PaginationLink>
|
|
1058
|
+
</PaginationItem>
|
|
1059
|
+
<PaginationItem>
|
|
1060
|
+
<PaginationLink href="#">2</PaginationLink>
|
|
1061
|
+
</PaginationItem>
|
|
1062
|
+
<PaginationItem>
|
|
1063
|
+
<PaginationLink href="#">3</PaginationLink>
|
|
1064
|
+
</PaginationItem>
|
|
1065
|
+
<PaginationItem>
|
|
1066
|
+
<PaginationNext href="#" />
|
|
1067
|
+
</PaginationItem>
|
|
1068
|
+
</PaginationContent>
|
|
1069
|
+
</Pagination>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/templates/ddd/page.ts
|
|
1078
|
+
function generatePage(config) {
|
|
1079
|
+
const { pageName, moduleName, entityName } = config;
|
|
1080
|
+
return `import { Suspense } from 'react';
|
|
1081
|
+
import { ${entityName}List } from '@/modules/${moduleName}/presentation/components/${moduleName}-list';
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* ${pageName} Page
|
|
1085
|
+
* Generated by shadcn-page-gen
|
|
1086
|
+
*/
|
|
1087
|
+
export default function ${entityName}Page() {
|
|
1088
|
+
return (
|
|
1089
|
+
<div className="container mx-auto py-10 space-y-8">
|
|
1090
|
+
<div>
|
|
1091
|
+
<h1 className="text-3xl font-bold tracking-tight">${pageName}</h1>
|
|
1092
|
+
<p className="text-muted-foreground">
|
|
1093
|
+
Manage your ${pageName.toLowerCase()}.
|
|
1094
|
+
</p>
|
|
1095
|
+
</div>
|
|
1096
|
+
|
|
1097
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
1098
|
+
<${entityName}List />
|
|
1099
|
+
</Suspense>
|
|
1100
|
+
</div>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/templates/ddd/template.ts
|
|
1107
|
+
function generateTemplate(config) {
|
|
1108
|
+
const { animations } = config;
|
|
1109
|
+
const getAnimationConfig = () => {
|
|
1110
|
+
switch (animations.intensity) {
|
|
1111
|
+
case "subtle":
|
|
1112
|
+
return {
|
|
1113
|
+
initial: "{ opacity: 0, y: 10 }",
|
|
1114
|
+
animate: "{ opacity: 1, y: 0 }",
|
|
1115
|
+
transition: "{ duration: 0.2 }"
|
|
1116
|
+
};
|
|
1117
|
+
case "bold":
|
|
1118
|
+
return {
|
|
1119
|
+
initial: "{ opacity: 0, scale: 0.95, y: 30 }",
|
|
1120
|
+
animate: "{ opacity: 1, scale: 1, y: 0 }",
|
|
1121
|
+
transition: "{ duration: 0.4, ease: [0.43, 0.13, 0.23, 0.96] }"
|
|
1122
|
+
};
|
|
1123
|
+
default:
|
|
1124
|
+
return {
|
|
1125
|
+
initial: "{ opacity: 0, y: 20 }",
|
|
1126
|
+
animate: "{ opacity: 1, y: 0 }",
|
|
1127
|
+
transition: '{ duration: 0.3, ease: "easeInOut" }'
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const animConfig = getAnimationConfig();
|
|
1132
|
+
return `'use client';
|
|
1133
|
+
|
|
1134
|
+
import { motion } from 'framer-motion';
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Page Template with Framer Motion transitions
|
|
1138
|
+
* Generated by shadcn-page-gen
|
|
1139
|
+
*/
|
|
1140
|
+
export default function Template({ children }: { children: React.ReactNode }) {
|
|
1141
|
+
return (
|
|
1142
|
+
<motion.div
|
|
1143
|
+
initial=${animConfig.initial}
|
|
1144
|
+
animate=${animConfig.animate}
|
|
1145
|
+
exit={{ opacity: 0, y: -20 }}
|
|
1146
|
+
transition=${animConfig.transition}
|
|
1147
|
+
>
|
|
1148
|
+
{children}
|
|
1149
|
+
</motion.div>
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/generators/ddd-generator.ts
|
|
1156
|
+
var DDDGenerator = class {
|
|
1157
|
+
constructor(config) {
|
|
1158
|
+
this.config = config;
|
|
1159
|
+
}
|
|
1160
|
+
async generate() {
|
|
1161
|
+
const files = [];
|
|
1162
|
+
const cwd = process.cwd();
|
|
1163
|
+
const { moduleName, routePath } = this.config;
|
|
1164
|
+
const moduleDir = import_path2.default.join(cwd, "modules", moduleName);
|
|
1165
|
+
const appDir = import_path2.default.join(cwd, "app", "(dashboard)", routePath);
|
|
1166
|
+
const dirs = [
|
|
1167
|
+
import_path2.default.join(moduleDir, "domain", "entities"),
|
|
1168
|
+
import_path2.default.join(moduleDir, "domain", "repositories"),
|
|
1169
|
+
import_path2.default.join(moduleDir, "application", "use-cases"),
|
|
1170
|
+
import_path2.default.join(moduleDir, "infrastructure", "repositories"),
|
|
1171
|
+
import_path2.default.join(moduleDir, "presentation", "components"),
|
|
1172
|
+
appDir
|
|
1173
|
+
];
|
|
1174
|
+
await createDirectories(dirs);
|
|
1175
|
+
files.push({
|
|
1176
|
+
path: import_path2.default.join(moduleDir, "domain", "entities", `${moduleName}.entity.ts`),
|
|
1177
|
+
content: generateEntity(this.config)
|
|
1178
|
+
});
|
|
1179
|
+
files.push({
|
|
1180
|
+
path: import_path2.default.join(moduleDir, "domain", "repositories", `${moduleName}.repository.interface.ts`),
|
|
1181
|
+
content: generateRepositoryInterface(this.config)
|
|
1182
|
+
});
|
|
1183
|
+
files.push({
|
|
1184
|
+
path: import_path2.default.join(moduleDir, "infrastructure", "repositories", `${moduleName}.repository.ts`),
|
|
1185
|
+
content: generateRepositoryImpl(this.config)
|
|
1186
|
+
});
|
|
1187
|
+
files.push({
|
|
1188
|
+
path: import_path2.default.join(moduleDir, "application", "use-cases", `get-${moduleName}s.use-case.ts`),
|
|
1189
|
+
content: generateUseCase(this.config)
|
|
1190
|
+
});
|
|
1191
|
+
files.push({
|
|
1192
|
+
path: import_path2.default.join(moduleDir, "presentation", "components", `${moduleName}-list.tsx`),
|
|
1193
|
+
content: generateComponent(this.config)
|
|
1194
|
+
});
|
|
1195
|
+
files.push({
|
|
1196
|
+
path: import_path2.default.join(appDir, "page.tsx"),
|
|
1197
|
+
content: generatePage(this.config)
|
|
1198
|
+
});
|
|
1199
|
+
if (this.config.animations.pageTransitions) {
|
|
1200
|
+
files.push({
|
|
1201
|
+
path: import_path2.default.join(appDir, "template.tsx"),
|
|
1202
|
+
content: generateTemplate(this.config)
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
await writeFiles(files);
|
|
1206
|
+
const instructions = this.generateInstructions();
|
|
1207
|
+
return { files, instructions };
|
|
1208
|
+
}
|
|
1209
|
+
generateInstructions() {
|
|
1210
|
+
const instructions = [];
|
|
1211
|
+
instructions.push(`Navigate to your page: http://localhost:3000/${this.config.routePath}`);
|
|
1212
|
+
const deps = [];
|
|
1213
|
+
if (this.config.dataFetching === "tanstack") {
|
|
1214
|
+
deps.push("@tanstack/react-query");
|
|
1215
|
+
}
|
|
1216
|
+
if (this.config.animations.pageTransitions || this.config.animations.listAnimations) {
|
|
1217
|
+
deps.push("framer-motion");
|
|
1218
|
+
}
|
|
1219
|
+
if (deps.length > 0) {
|
|
1220
|
+
instructions.push(`Install dependencies: npm install ${deps.join(" ")}`);
|
|
1221
|
+
}
|
|
1222
|
+
instructions.push("Customize the generated code to fit your needs");
|
|
1223
|
+
instructions.push("Connect to your real API (replace mock repository)");
|
|
1224
|
+
if (this.config.dataFetching === "tanstack") {
|
|
1225
|
+
instructions.push("Ensure your app is wrapped in <QueryClientProvider>");
|
|
1226
|
+
}
|
|
1227
|
+
return instructions;
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
// src/generators/simplified-generator.ts
|
|
1232
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
1233
|
+
|
|
1234
|
+
// src/templates/simplified/component.ts
|
|
1235
|
+
function generateSimplifiedComponent(config) {
|
|
1236
|
+
const {
|
|
1237
|
+
entityName,
|
|
1238
|
+
moduleName,
|
|
1239
|
+
pageName,
|
|
1240
|
+
routePath,
|
|
1241
|
+
columns,
|
|
1242
|
+
filters,
|
|
1243
|
+
includeStats,
|
|
1244
|
+
includeRowSelection,
|
|
1245
|
+
dataFetching,
|
|
1246
|
+
sortableColumns,
|
|
1247
|
+
animations
|
|
1248
|
+
} = config;
|
|
1249
|
+
const isTanStack = dataFetching === "tanstack";
|
|
1250
|
+
const hasAnimations = animations.listAnimations || animations.cardAnimations;
|
|
1251
|
+
const mockDataInterface = `interface ${entityName} {
|
|
1252
|
+
id: string;
|
|
1253
|
+
${columns.map((c) => {
|
|
1254
|
+
const typeMap = {
|
|
1255
|
+
"string": "string",
|
|
1256
|
+
"number": "number",
|
|
1257
|
+
"boolean": "boolean",
|
|
1258
|
+
"date": "string"
|
|
1259
|
+
};
|
|
1260
|
+
return ` ${c.key}: ${typeMap[c.type]};`;
|
|
1261
|
+
}).join("\n")}
|
|
1262
|
+
updatedAt: Date;
|
|
1263
|
+
}`;
|
|
1264
|
+
const mockDataFields = columns.map((c) => {
|
|
1265
|
+
if (c.key === "createdAt") {
|
|
1266
|
+
return ` createdAt: new Date().toISOString(),`;
|
|
1267
|
+
}
|
|
1268
|
+
if (c.key === "status") {
|
|
1269
|
+
return ` status: Math.random() > 0.5 ? 'Active' : 'Inactive',`;
|
|
1270
|
+
}
|
|
1271
|
+
if (c.key === "name") {
|
|
1272
|
+
return ` name: \`${entityName} \${i + 1}\`,`;
|
|
1273
|
+
}
|
|
1274
|
+
if (c.type === "number") {
|
|
1275
|
+
return ` ${c.key}: (i + 1) * 100,`;
|
|
1276
|
+
}
|
|
1277
|
+
if (c.type === "boolean") {
|
|
1278
|
+
return ` ${c.key}: Math.random() > 0.5,`;
|
|
1279
|
+
}
|
|
1280
|
+
if (c.type === "date") {
|
|
1281
|
+
return ` ${c.key}: new Date().toISOString(),`;
|
|
1282
|
+
}
|
|
1283
|
+
return ` ${c.key}: \`${c.label} \${i + 1}\`,`;
|
|
1284
|
+
}).join("\n");
|
|
1285
|
+
const mockData = `// Mock data - replace with your API call
|
|
1286
|
+
const MOCK_DATA: ${entityName}[] = Array.from({ length: 10 }).map((_, i) => ({
|
|
1287
|
+
id: (i + 1).toString(),
|
|
1288
|
+
${mockDataFields}
|
|
1289
|
+
updatedAt: new Date(),
|
|
1290
|
+
})) as unknown as ${entityName}[];`;
|
|
1291
|
+
const fetchFunction = isTanStack ? "" : `
|
|
1292
|
+
// Fetch data function - replace with your API call
|
|
1293
|
+
const fetchData = async () => {
|
|
1294
|
+
setLoading(true);
|
|
1295
|
+
try {
|
|
1296
|
+
// Simulate API delay
|
|
1297
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1298
|
+
|
|
1299
|
+
// TODO: Replace with real API call
|
|
1300
|
+
// const response = await fetch('/api/${moduleName}');
|
|
1301
|
+
// const result = await response.json();
|
|
1302
|
+
|
|
1303
|
+
setData(MOCK_DATA);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
console.error('Error fetching data:', error);
|
|
1306
|
+
} finally {
|
|
1307
|
+
setLoading(false);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
useEffect(() => {
|
|
1312
|
+
fetchData();
|
|
1313
|
+
}, [searchParams]);`;
|
|
1314
|
+
const dataFetchingSetup = isTanStack ? `
|
|
1315
|
+
// TanStack Query data fetching
|
|
1316
|
+
const { data, isLoading: loading, refetch } = useQuery({
|
|
1317
|
+
queryKey: ['${moduleName}', searchParams.toString()],
|
|
1318
|
+
queryFn: async () => {
|
|
1319
|
+
// Simulate API delay
|
|
1320
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1321
|
+
|
|
1322
|
+
// TODO: Replace with real API call
|
|
1323
|
+
// const response = await fetch('/api/${moduleName}');
|
|
1324
|
+
// return await response.json();
|
|
1325
|
+
|
|
1326
|
+
return MOCK_DATA;
|
|
1327
|
+
}
|
|
1328
|
+
});` : `
|
|
1329
|
+
const [data, setData] = useState<${entityName}[]>([]);
|
|
1330
|
+
const [loading, setLoading] = useState(true);
|
|
1331
|
+
${fetchFunction}`;
|
|
1332
|
+
const imports = `'use client';
|
|
1333
|
+
|
|
1334
|
+
${hasAnimations ? `import { motion } from 'framer-motion';` : ""}
|
|
1335
|
+
import { useEffect, useState } from 'react';
|
|
1336
|
+
${isTanStack ? `import { useQuery } from '@tanstack/react-query';` : ""}
|
|
1337
|
+
import { Button } from '@/components/ui/button';
|
|
1338
|
+
import {
|
|
1339
|
+
Table,
|
|
1340
|
+
TableBody,
|
|
1341
|
+
TableCell,
|
|
1342
|
+
TableHead,
|
|
1343
|
+
TableHeader,
|
|
1344
|
+
TableRow,
|
|
1345
|
+
} from '@/components/ui/table';
|
|
1346
|
+
import {
|
|
1347
|
+
Select,
|
|
1348
|
+
SelectContent,
|
|
1349
|
+
SelectItem,
|
|
1350
|
+
SelectTrigger,
|
|
1351
|
+
SelectValue,
|
|
1352
|
+
} from '@/components/ui/select';
|
|
1353
|
+
import { Input } from '@/components/ui/input';
|
|
1354
|
+
${filters.some((f) => f.type === "date") ? `import { Calendar } from '@/components/ui/calendar';
|
|
1355
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
1356
|
+
import { Calendar as CalendarIcon } from 'lucide-react';
|
|
1357
|
+
import { format } from 'date-fns';` : ""}
|
|
1358
|
+
import { Badge } from '@/components/ui/badge';
|
|
1359
|
+
${includeStats ? `import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';` : ""}
|
|
1360
|
+
import {
|
|
1361
|
+
Search,
|
|
1362
|
+
Plus,
|
|
1363
|
+
X,
|
|
1364
|
+
RefreshCcw,
|
|
1365
|
+
Eye,
|
|
1366
|
+
Pencil,
|
|
1367
|
+
Trash2,
|
|
1368
|
+
MoreHorizontal,
|
|
1369
|
+
${sortableColumns.length > 0 ? "ArrowUpDown, ArrowUp, ArrowDown," : ""}
|
|
1370
|
+
} from 'lucide-react';
|
|
1371
|
+
import { cn } from '@/lib/utils';
|
|
1372
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
1373
|
+
import {
|
|
1374
|
+
Pagination,
|
|
1375
|
+
PaginationContent,
|
|
1376
|
+
PaginationItem,
|
|
1377
|
+
PaginationLink,
|
|
1378
|
+
PaginationNext,
|
|
1379
|
+
PaginationPrevious,
|
|
1380
|
+
} from "@/components/ui/pagination";
|
|
1381
|
+
import {
|
|
1382
|
+
DropdownMenu,
|
|
1383
|
+
DropdownMenuContent,
|
|
1384
|
+
DropdownMenuItem,
|
|
1385
|
+
DropdownMenuLabel,
|
|
1386
|
+
DropdownMenuTrigger,
|
|
1387
|
+
} from "@/components/ui/dropdown-menu";
|
|
1388
|
+
${includeRowSelection ? `import { Checkbox } from "@/components/ui/checkbox";` : ""}`;
|
|
1389
|
+
const animationVariants = hasAnimations ? `
|
|
1390
|
+
// Framer Motion animation variants
|
|
1391
|
+
const containerVariants = {
|
|
1392
|
+
hidden: { opacity: 0 },
|
|
1393
|
+
visible: {
|
|
1394
|
+
opacity: 1,
|
|
1395
|
+
transition: {
|
|
1396
|
+
staggerChildren: ${animations.intensity === "bold" ? "0.1" : animations.intensity === "subtle" ? "0.03" : "0.05"}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
const itemVariants = {
|
|
1402
|
+
hidden: { opacity: 0, x: -20 },
|
|
1403
|
+
visible: {
|
|
1404
|
+
opacity: 1,
|
|
1405
|
+
x: 0,
|
|
1406
|
+
transition: { duration: ${animations.intensity === "bold" ? "0.3" : animations.intensity === "subtle" ? "0.15" : "0.2"} }
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
` : "";
|
|
1410
|
+
const rowSelectionCode = includeRowSelection ? `
|
|
1411
|
+
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
|
1412
|
+
|
|
1413
|
+
const toggleRow = (id: string) => {
|
|
1414
|
+
const newSelected = new Set(selectedRows);
|
|
1415
|
+
if (newSelected.has(id)) newSelected.delete(id);
|
|
1416
|
+
else newSelected.add(id);
|
|
1417
|
+
setSelectedRows(newSelected);
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const toggleAll = () => {
|
|
1421
|
+
if (selectedRows.size === (data?.length || 0)) {
|
|
1422
|
+
setSelectedRows(new Set());
|
|
1423
|
+
} else {
|
|
1424
|
+
setSelectedRows(new Set(data?.map(i => i.id) || []));
|
|
1425
|
+
}
|
|
1426
|
+
};` : "";
|
|
1427
|
+
const statsCards = includeStats ? `
|
|
1428
|
+
{/* Stats Cards */}
|
|
1429
|
+
${animations.cardAnimations ? `<motion.div
|
|
1430
|
+
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
|
1431
|
+
variants={containerVariants}
|
|
1432
|
+
initial="hidden"
|
|
1433
|
+
animate="visible"
|
|
1434
|
+
>` : `<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">`}
|
|
1435
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
1436
|
+
<Card>
|
|
1437
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
1438
|
+
<CardTitle className="text-sm font-medium">Total ${entityName}s</CardTitle>
|
|
1439
|
+
</CardHeader>
|
|
1440
|
+
<CardContent>
|
|
1441
|
+
<div className="text-2xl font-bold">128</div>
|
|
1442
|
+
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
|
|
1443
|
+
</CardContent>
|
|
1444
|
+
</Card>
|
|
1445
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
1446
|
+
|
|
1447
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
1448
|
+
<Card>
|
|
1449
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
1450
|
+
<CardTitle className="text-sm font-medium">Active</CardTitle>
|
|
1451
|
+
</CardHeader>
|
|
1452
|
+
<CardContent>
|
|
1453
|
+
<div className="text-2xl font-bold">12</div>
|
|
1454
|
+
<p className="text-xs text-muted-foreground">+4 since yesterday</p>
|
|
1455
|
+
</CardContent>
|
|
1456
|
+
</Card>
|
|
1457
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
1458
|
+
|
|
1459
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
1460
|
+
<Card>
|
|
1461
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
1462
|
+
<CardTitle className="text-sm font-medium">Pending</CardTitle>
|
|
1463
|
+
</CardHeader>
|
|
1464
|
+
<CardContent>
|
|
1465
|
+
<div className="text-2xl font-bold">4</div>
|
|
1466
|
+
<p className="text-xs text-muted-foreground">-2 since yesterday</p>
|
|
1467
|
+
</CardContent>
|
|
1468
|
+
</Card>
|
|
1469
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
1470
|
+
|
|
1471
|
+
${animations.cardAnimations ? `<motion.div variants={itemVariants}>` : ""}
|
|
1472
|
+
<Card>
|
|
1473
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
1474
|
+
<CardTitle className="text-sm font-medium">Closed</CardTitle>
|
|
1475
|
+
</CardHeader>
|
|
1476
|
+
<CardContent>
|
|
1477
|
+
<div className="text-2xl font-bold">2</div>
|
|
1478
|
+
<p className="text-xs text-muted-foreground">+1 since yesterday</p>
|
|
1479
|
+
</CardContent>
|
|
1480
|
+
</Card>
|
|
1481
|
+
${animations.cardAnimations ? `</motion.div>` : ""}
|
|
1482
|
+
${animations.cardAnimations ? `</motion.div>` : `</div>`}
|
|
1483
|
+
` : "";
|
|
1484
|
+
const filterComponents = filters.map((f) => {
|
|
1485
|
+
if (f.type === "select") {
|
|
1486
|
+
return `
|
|
1487
|
+
<Select
|
|
1488
|
+
value={searchParams.get('${f.key}') || 'all'}
|
|
1489
|
+
onValueChange={(value) => updateParam('${f.key}', value === 'all' ? null : value)}
|
|
1490
|
+
>
|
|
1491
|
+
<SelectTrigger className="w-[150px]">
|
|
1492
|
+
<SelectValue placeholder="${f.label}" />
|
|
1493
|
+
</SelectTrigger>
|
|
1494
|
+
<SelectContent>
|
|
1495
|
+
<SelectItem value="all">All ${f.label}</SelectItem>
|
|
1496
|
+
<SelectItem value="active">Active</SelectItem>
|
|
1497
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
1498
|
+
</SelectContent>
|
|
1499
|
+
</Select>`;
|
|
1500
|
+
} else if (f.type === "date") {
|
|
1501
|
+
return `
|
|
1502
|
+
<Popover>
|
|
1503
|
+
<PopoverTrigger asChild>
|
|
1504
|
+
<Button
|
|
1505
|
+
variant="outline"
|
|
1506
|
+
className={cn(
|
|
1507
|
+
"w-[200px] justify-start text-left font-normal",
|
|
1508
|
+
!searchParams.get('${f.key}') && "text-muted-foreground"
|
|
1509
|
+
)}
|
|
1510
|
+
>
|
|
1511
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
1512
|
+
{searchParams.get('${f.key}') ? (
|
|
1513
|
+
format(new Date(searchParams.get('${f.key}')!), "PPP")
|
|
1514
|
+
) : (
|
|
1515
|
+
<span>${f.label}</span>
|
|
1516
|
+
)}
|
|
1517
|
+
</Button>
|
|
1518
|
+
</PopoverTrigger>
|
|
1519
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
1520
|
+
<Calendar
|
|
1521
|
+
mode="single"
|
|
1522
|
+
selected={searchParams.get('${f.key}') ? new Date(searchParams.get('${f.key}')!) : undefined}
|
|
1523
|
+
onSelect={(date) => updateParam('${f.key}', date ? date.toISOString() : null)}
|
|
1524
|
+
initialFocus
|
|
1525
|
+
/>
|
|
1526
|
+
</PopoverContent>
|
|
1527
|
+
</Popover>`;
|
|
1528
|
+
}
|
|
1529
|
+
return "";
|
|
1530
|
+
}).join("\n");
|
|
1531
|
+
const tableHeaders = columns.map((c) => {
|
|
1532
|
+
const isSortable = sortableColumns.includes(c.key);
|
|
1533
|
+
if (isSortable) {
|
|
1534
|
+
return ` <TableHead className="cursor-pointer" onClick={() => handleSort('${c.key}')}>
|
|
1535
|
+
<div className="flex items-center gap-1">
|
|
1536
|
+
${c.label.toUpperCase()}
|
|
1537
|
+
{searchParams.get('sortBy') === '${c.key}' ? (
|
|
1538
|
+
searchParams.get('order') === 'asc' ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />
|
|
1539
|
+
) : <ArrowUpDown className="h-3 w-3 text-muted-foreground" />}
|
|
1540
|
+
</div>
|
|
1541
|
+
</TableHead>`;
|
|
1542
|
+
}
|
|
1543
|
+
return ` <TableHead>${c.label.toUpperCase()}</TableHead>`;
|
|
1544
|
+
}).join("\n");
|
|
1545
|
+
const tableCells = columns.map((c) => {
|
|
1546
|
+
if (c.key === "status") {
|
|
1547
|
+
return ` <TableCell>
|
|
1548
|
+
<Badge variant={item.status === 'Active' ? 'default' : 'secondary'} className="rounded-full">
|
|
1549
|
+
{item.status}
|
|
1550
|
+
</Badge>
|
|
1551
|
+
</TableCell>`;
|
|
1552
|
+
}
|
|
1553
|
+
if (c.type === "date") {
|
|
1554
|
+
return ` <TableCell className="text-muted-foreground">{new Date(item.${c.key}).toLocaleDateString()}</TableCell>`;
|
|
1555
|
+
}
|
|
1556
|
+
return ` <TableCell>{item.${c.key}}</TableCell>`;
|
|
1557
|
+
}).join("\n");
|
|
1558
|
+
const colSpan = columns.length + 2 + (includeRowSelection ? 1 : 0);
|
|
1559
|
+
return `${imports}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* ${entityName} List Component (Simplified Architecture)
|
|
1563
|
+
* Generated by shadcn-page-gen
|
|
1564
|
+
*/
|
|
1565
|
+
|
|
1566
|
+
${mockDataInterface}
|
|
1567
|
+
|
|
1568
|
+
${mockData}
|
|
1569
|
+
${animationVariants}
|
|
1570
|
+
export function ${entityName}List() {
|
|
1571
|
+
const router = useRouter();
|
|
1572
|
+
const searchParams = useSearchParams();
|
|
1573
|
+
|
|
1574
|
+
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '');
|
|
1575
|
+
const pageSize = Number(searchParams.get('limit')) || 10;
|
|
1576
|
+
${dataFetchingSetup}
|
|
1577
|
+
${rowSelectionCode}
|
|
1578
|
+
|
|
1579
|
+
const updateParam = (key: string, value: string | null) => {
|
|
1580
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
1581
|
+
if (value) params.set(key, value);
|
|
1582
|
+
else params.delete(key);
|
|
1583
|
+
|
|
1584
|
+
if (key !== 'page') params.set('page', '1');
|
|
1585
|
+
|
|
1586
|
+
router.push('?' + params.toString());
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
${sortableColumns.length > 0 ? `const handleSort = (key: string) => {
|
|
1590
|
+
const currentSort = searchParams.get('sortBy');
|
|
1591
|
+
const currentOrder = searchParams.get('order');
|
|
1592
|
+
|
|
1593
|
+
let newOrder = 'asc';
|
|
1594
|
+
if (currentSort === key && currentOrder === 'asc') {
|
|
1595
|
+
newOrder = 'desc';
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
1599
|
+
params.set('sortBy', key);
|
|
1600
|
+
params.set('order', newOrder);
|
|
1601
|
+
router.push('?' + params.toString());
|
|
1602
|
+
};` : ""}
|
|
1603
|
+
|
|
1604
|
+
return (
|
|
1605
|
+
<div className="space-y-6">
|
|
1606
|
+
${statsCards}
|
|
1607
|
+
{/* Actions Bar */}
|
|
1608
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
1609
|
+
<div className="flex flex-1 items-center gap-2">
|
|
1610
|
+
<div className="relative flex-1">
|
|
1611
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
1612
|
+
<Input
|
|
1613
|
+
placeholder="Search..."
|
|
1614
|
+
value={searchTerm}
|
|
1615
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
1616
|
+
onBlur={() => updateParam('q', searchTerm)}
|
|
1617
|
+
className="pl-9 w-full md:max-w-md"
|
|
1618
|
+
/>
|
|
1619
|
+
</div>
|
|
1620
|
+
${filterComponents}
|
|
1621
|
+
{(searchParams.toString().length > 0) && (
|
|
1622
|
+
<Button variant="ghost" size="icon" onClick={() => router.push('/${routePath}')} title="Reset Filters">
|
|
1623
|
+
<X className="h-4 w-4" />
|
|
1624
|
+
</Button>
|
|
1625
|
+
)}
|
|
1626
|
+
</div>
|
|
1627
|
+
|
|
1628
|
+
<div className="flex items-center gap-2">
|
|
1629
|
+
<Button variant="outline" size="sm" onClick={() => ${isTanStack ? "refetch()" : "fetchData()"}}>
|
|
1630
|
+
<RefreshCcw className="mr-2 h-4 w-4" /> Refresh
|
|
1631
|
+
</Button>
|
|
1632
|
+
<Button size="sm">
|
|
1633
|
+
<Plus className="mr-2 h-4 w-4" /> New ${entityName}
|
|
1634
|
+
</Button>
|
|
1635
|
+
</div>
|
|
1636
|
+
</div>
|
|
1637
|
+
|
|
1638
|
+
{/* Main Table */}
|
|
1639
|
+
<Card>
|
|
1640
|
+
<CardContent className="p-0">
|
|
1641
|
+
<Table>
|
|
1642
|
+
<TableHeader>
|
|
1643
|
+
<TableRow>
|
|
1644
|
+
${includeRowSelection ? `<TableHead className="w-[50px]"><Checkbox checked={data?.length > 0 && selectedRows.size === data?.length} onCheckedChange={toggleAll} /></TableHead>` : ""}
|
|
1645
|
+
<TableHead className="w-[100px]">ID</TableHead>
|
|
1646
|
+
${tableHeaders}
|
|
1647
|
+
<TableHead className="text-right">ACTIONS</TableHead>
|
|
1648
|
+
</TableRow>
|
|
1649
|
+
</TableHeader>
|
|
1650
|
+
<TableBody>
|
|
1651
|
+
{loading ? (
|
|
1652
|
+
<TableRow>
|
|
1653
|
+
<TableCell colSpan={${colSpan}} className="text-center h-24 text-muted-foreground">Loading data...</TableCell>
|
|
1654
|
+
</TableRow>
|
|
1655
|
+
) : !data || data.length === 0 ? (
|
|
1656
|
+
<TableRow>
|
|
1657
|
+
<TableCell colSpan={${colSpan}} className="text-center h-24 text-muted-foreground">No results found.</TableCell>
|
|
1658
|
+
</TableRow>
|
|
1659
|
+
) : (
|
|
1660
|
+
${animations.listAnimations ? `
|
|
1661
|
+
data.map((item, index) => (
|
|
1662
|
+
<motion.tr
|
|
1663
|
+
key={item.id}
|
|
1664
|
+
variants={itemVariants}
|
|
1665
|
+
initial="hidden"
|
|
1666
|
+
animate="visible"
|
|
1667
|
+
custom={index}
|
|
1668
|
+
className="group"
|
|
1669
|
+
>` : `data.map((item) => (
|
|
1670
|
+
<TableRow key={item.id} className="group">`}
|
|
1671
|
+
${includeRowSelection ? `<TableCell><Checkbox checked={selectedRows.has(item.id)} onCheckedChange={() => toggleRow(item.id)} /></TableCell>` : ""}
|
|
1672
|
+
<TableCell className="font-medium">{item.id}</TableCell>
|
|
1673
|
+
${tableCells}
|
|
1674
|
+
<TableCell className="text-right">
|
|
1675
|
+
<div className="flex justify-end gap-2">
|
|
1676
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950">
|
|
1677
|
+
<Eye className="h-4 w-4" />
|
|
1678
|
+
</Button>
|
|
1679
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-green-500 hover:text-green-600 hover:bg-green-50 dark:hover:bg-green-950">
|
|
1680
|
+
<Pencil className="h-4 w-4" />
|
|
1681
|
+
</Button>
|
|
1682
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950">
|
|
1683
|
+
<Trash2 className="h-4 w-4" />
|
|
1684
|
+
</Button>
|
|
1685
|
+
<DropdownMenu>
|
|
1686
|
+
<DropdownMenuTrigger asChild>
|
|
1687
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
1688
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
1689
|
+
</Button>
|
|
1690
|
+
</DropdownMenuTrigger>
|
|
1691
|
+
<DropdownMenuContent align="end">
|
|
1692
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
1693
|
+
<DropdownMenuItem>View Details</DropdownMenuItem>
|
|
1694
|
+
<DropdownMenuItem>Edit Record</DropdownMenuItem>
|
|
1695
|
+
</DropdownMenuContent>
|
|
1696
|
+
</DropdownMenu>
|
|
1697
|
+
</div>
|
|
1698
|
+
</TableCell>
|
|
1699
|
+
${animations.listAnimations ? `</motion.tr>` : `</TableRow>`}
|
|
1700
|
+
))
|
|
1701
|
+
)}
|
|
1702
|
+
</TableBody>
|
|
1703
|
+
</Table>
|
|
1704
|
+
</CardContent>
|
|
1705
|
+
</Card>
|
|
1706
|
+
|
|
1707
|
+
{/* Pagination */}
|
|
1708
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
1709
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
1710
|
+
<span>Show</span>
|
|
1711
|
+
<Select value={pageSize.toString()} onValueChange={(v) => updateParam('limit', v)}>
|
|
1712
|
+
<SelectTrigger className="h-8 w-[70px]">
|
|
1713
|
+
<SelectValue placeholder={pageSize.toString()} />
|
|
1714
|
+
</SelectTrigger>
|
|
1715
|
+
<SelectContent side="top">
|
|
1716
|
+
{[10, 20, 30, 50, 100].map((size) => (
|
|
1717
|
+
<SelectItem key={size} value={size.toString()}>
|
|
1718
|
+
{size}
|
|
1719
|
+
</SelectItem>
|
|
1720
|
+
))}
|
|
1721
|
+
</SelectContent>
|
|
1722
|
+
</Select>
|
|
1723
|
+
<span>entries</span>
|
|
1724
|
+
</div>
|
|
1725
|
+
<Pagination className="justify-end w-auto">
|
|
1726
|
+
<PaginationContent>
|
|
1727
|
+
<PaginationItem>
|
|
1728
|
+
<PaginationPrevious href="#" />
|
|
1729
|
+
</PaginationItem>
|
|
1730
|
+
<PaginationItem>
|
|
1731
|
+
<PaginationLink href="#" isActive>1</PaginationLink>
|
|
1732
|
+
</PaginationItem>
|
|
1733
|
+
<PaginationItem>
|
|
1734
|
+
<PaginationLink href="#">2</PaginationLink>
|
|
1735
|
+
</PaginationItem>
|
|
1736
|
+
<PaginationItem>
|
|
1737
|
+
<PaginationLink href="#">3</PaginationLink>
|
|
1738
|
+
</PaginationItem>
|
|
1739
|
+
<PaginationItem>
|
|
1740
|
+
<PaginationNext href="#" />
|
|
1741
|
+
</PaginationItem>
|
|
1742
|
+
</PaginationContent>
|
|
1743
|
+
</Pagination>
|
|
1744
|
+
</div>
|
|
1745
|
+
</div>
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
`;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/templates/simplified/page.ts
|
|
1752
|
+
function generateSimplifiedPage(config) {
|
|
1753
|
+
const { pageName, moduleName, entityName } = config;
|
|
1754
|
+
return `import { Suspense } from 'react';
|
|
1755
|
+
import { ${entityName}List } from '@/components/${moduleName}/${moduleName}-list';
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* ${pageName} Page
|
|
1759
|
+
* Generated by shadcn-page-gen (Simplified Architecture)
|
|
1760
|
+
*/
|
|
1761
|
+
export default function ${entityName}Page() {
|
|
1762
|
+
return (
|
|
1763
|
+
<div className="container mx-auto py-10 space-y-8">
|
|
1764
|
+
<div>
|
|
1765
|
+
<h1 className="text-3xl font-bold tracking-tight">${pageName}</h1>
|
|
1766
|
+
<p className="text-muted-foreground">
|
|
1767
|
+
Manage your ${pageName.toLowerCase()}.
|
|
1768
|
+
</p>
|
|
1769
|
+
</div>
|
|
1770
|
+
|
|
1771
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
1772
|
+
<${entityName}List />
|
|
1773
|
+
</Suspense>
|
|
1774
|
+
</div>
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
`;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/generators/simplified-generator.ts
|
|
1781
|
+
var SimplifiedGenerator = class {
|
|
1782
|
+
constructor(config) {
|
|
1783
|
+
this.config = config;
|
|
1784
|
+
}
|
|
1785
|
+
async generate() {
|
|
1786
|
+
const files = [];
|
|
1787
|
+
const cwd = process.cwd();
|
|
1788
|
+
const { moduleName, routePath } = this.config;
|
|
1789
|
+
const componentDir = import_path3.default.join(cwd, "components", moduleName);
|
|
1790
|
+
const appDir = import_path3.default.join(cwd, "app", "(dashboard)", routePath);
|
|
1791
|
+
await createDirectories([componentDir, appDir]);
|
|
1792
|
+
files.push({
|
|
1793
|
+
path: import_path3.default.join(componentDir, `${moduleName}-list.tsx`),
|
|
1794
|
+
content: generateSimplifiedComponent(this.config)
|
|
1795
|
+
});
|
|
1796
|
+
files.push({
|
|
1797
|
+
path: import_path3.default.join(appDir, "page.tsx"),
|
|
1798
|
+
content: generateSimplifiedPage(this.config)
|
|
1799
|
+
});
|
|
1800
|
+
if (this.config.animations.pageTransitions) {
|
|
1801
|
+
files.push({
|
|
1802
|
+
path: import_path3.default.join(appDir, "template.tsx"),
|
|
1803
|
+
content: generateTemplate(this.config)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
await writeFiles(files);
|
|
1807
|
+
const instructions = this.generateInstructions();
|
|
1808
|
+
return { files, instructions };
|
|
1809
|
+
}
|
|
1810
|
+
generateInstructions() {
|
|
1811
|
+
const instructions = [];
|
|
1812
|
+
instructions.push(`Navigate to your page: http://localhost:3000/${this.config.routePath}`);
|
|
1813
|
+
const deps = [];
|
|
1814
|
+
if (this.config.dataFetching === "tanstack") {
|
|
1815
|
+
deps.push("@tanstack/react-query");
|
|
1816
|
+
}
|
|
1817
|
+
if (this.config.animations.pageTransitions || this.config.animations.listAnimations) {
|
|
1818
|
+
deps.push("framer-motion");
|
|
1819
|
+
}
|
|
1820
|
+
if (deps.length > 0) {
|
|
1821
|
+
instructions.push(`Install dependencies: npm install ${deps.join(" ")}`);
|
|
1822
|
+
}
|
|
1823
|
+
instructions.push("Customize the generated code to fit your needs");
|
|
1824
|
+
instructions.push("Replace mock data with your real API");
|
|
1825
|
+
if (this.config.dataFetching === "tanstack") {
|
|
1826
|
+
instructions.push("Ensure your app is wrapped in <QueryClientProvider>");
|
|
1827
|
+
}
|
|
1828
|
+
return instructions;
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
// src/generators/index.ts
|
|
1833
|
+
var PageGenerator = class {
|
|
1834
|
+
constructor(config) {
|
|
1835
|
+
this.config = config;
|
|
1836
|
+
this.generator = config.architecture === "ddd" ? new DDDGenerator(config) : new SimplifiedGenerator(config);
|
|
1837
|
+
}
|
|
1838
|
+
generator;
|
|
1839
|
+
/**
|
|
1840
|
+
* Generates all files and returns result
|
|
1841
|
+
*/
|
|
1842
|
+
async generate() {
|
|
1843
|
+
return await this.generator.generate();
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
// src/cli/index.ts
|
|
1848
|
+
async function run() {
|
|
1849
|
+
try {
|
|
1850
|
+
console.clear();
|
|
1851
|
+
logger.title("\u{1F3A8} shadcn-page-gen");
|
|
1852
|
+
logger.dim("Generate production-ready Next.js pages with shadcn/ui, Tailwind v4, and Framer Motion\n");
|
|
1853
|
+
const config = await collectConfiguration();
|
|
1854
|
+
if (!config) {
|
|
1855
|
+
logger.warning("Operation cancelled");
|
|
1856
|
+
process.exit(0);
|
|
1857
|
+
}
|
|
1858
|
+
console.log("\n");
|
|
1859
|
+
logger.title("\u{1F4CB} Configuration Summary");
|
|
1860
|
+
logger.info(`Page Name: ${config.pageName}`);
|
|
1861
|
+
logger.info(`Route: /${config.routePath}`);
|
|
1862
|
+
logger.info(`Architecture: ${config.architecture.toUpperCase()}`);
|
|
1863
|
+
logger.info(`Data Fetching: ${config.dataFetching}`);
|
|
1864
|
+
logger.info(`Animations: ${config.animations.intensity} intensity`);
|
|
1865
|
+
console.log("");
|
|
1866
|
+
const spinner = (0, import_ora.default)("Generating files...").start();
|
|
1867
|
+
try {
|
|
1868
|
+
const generator = new PageGenerator(config);
|
|
1869
|
+
const result = await generator.generate();
|
|
1870
|
+
spinner.succeed("Files generated successfully!");
|
|
1871
|
+
console.log("");
|
|
1872
|
+
logger.title("\u{1F4C1} Generated Files");
|
|
1873
|
+
result.files.forEach((file) => {
|
|
1874
|
+
logger.dim(` ${file.path}`);
|
|
1875
|
+
});
|
|
1876
|
+
console.log("");
|
|
1877
|
+
logger.title("\u{1F389} Success!");
|
|
1878
|
+
logger.success("Your page has been generated!");
|
|
1879
|
+
console.log("");
|
|
1880
|
+
logger.title("\u{1F4DD} Next Steps");
|
|
1881
|
+
result.instructions.forEach((instruction, index) => {
|
|
1882
|
+
logger.step(index + 1, result.instructions.length, instruction);
|
|
1883
|
+
});
|
|
1884
|
+
console.log("");
|
|
1885
|
+
logger.dim("Happy coding! \u{1F680}\n");
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
spinner.fail("Generation failed");
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
logger.error("An error occurred:");
|
|
1892
|
+
console.error(error);
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1897
|
+
0 && (module.exports = {
|
|
1898
|
+
run
|
|
1899
|
+
});
|
|
1900
|
+
//# sourceMappingURL=index.cjs.map
|