@erwininteractive/mvc 0.1.5 → 0.2.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/dist/cli.js +21 -2
- package/dist/generators/generateResource.d.ts +11 -0
- package/dist/generators/generateResource.js +199 -0
- package/dist/generators/utils.d.ts +19 -0
- package/dist/generators/utils.js +80 -0
- package/package.json +1 -1
- package/templates/controller.resource.ts.ejs +181 -0
- package/templates/views/create.ejs.ejs +137 -0
- package/templates/views/edit.ejs.ejs +157 -0
- package/templates/views/index.ejs.ejs +97 -0
- package/templates/views/show.ejs.ejs +97 -0
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,12 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const initApp_1 = require("./generators/initApp");
|
|
6
6
|
const generateModel_1 = require("./generators/generateModel");
|
|
7
7
|
const generateController_1 = require("./generators/generateController");
|
|
8
|
+
const generateResource_1 = require("./generators/generateResource");
|
|
8
9
|
const program = new commander_1.Command();
|
|
9
10
|
program
|
|
10
11
|
.name("erwinmvc")
|
|
11
12
|
.description("CLI for @erwininteractive/mvc framework")
|
|
12
|
-
.version("0.1.
|
|
13
|
+
.version("0.1.5");
|
|
13
14
|
// Init command - scaffold a new application
|
|
14
15
|
program
|
|
15
16
|
.command("init <dir>")
|
|
@@ -29,7 +30,7 @@ program
|
|
|
29
30
|
const generate = program
|
|
30
31
|
.command("generate")
|
|
31
32
|
.alias("g")
|
|
32
|
-
.description("Generate models or
|
|
33
|
+
.description("Generate models, controllers, or resources");
|
|
33
34
|
// Generate model
|
|
34
35
|
generate
|
|
35
36
|
.command("model <name>")
|
|
@@ -58,4 +59,22 @@ generate
|
|
|
58
59
|
process.exit(1);
|
|
59
60
|
}
|
|
60
61
|
});
|
|
62
|
+
// Generate resource (model + controller + views)
|
|
63
|
+
generate
|
|
64
|
+
.command("resource <name>")
|
|
65
|
+
.description("Generate a complete resource (model + controller + views)")
|
|
66
|
+
.option("--skip-model", "Skip generating Prisma model")
|
|
67
|
+
.option("--skip-controller", "Skip generating controller")
|
|
68
|
+
.option("--skip-views", "Skip generating views")
|
|
69
|
+
.option("--skip-migrate", "Skip running Prisma migrate")
|
|
70
|
+
.option("--api-only", "Generate API-only controller (no views)")
|
|
71
|
+
.action(async (name, options) => {
|
|
72
|
+
try {
|
|
73
|
+
await (0, generateResource_1.generateResource)(name, options);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
61
80
|
program.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface GenerateResourceOptions {
|
|
2
|
+
skipModel?: boolean;
|
|
3
|
+
skipController?: boolean;
|
|
4
|
+
skipViews?: boolean;
|
|
5
|
+
skipMigrate?: boolean;
|
|
6
|
+
apiOnly?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generate a complete resource: model + controller + views.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateResource(name: string, options?: GenerateResourceOptions): Promise<void>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateResource = generateResource;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const ejs_1 = __importDefault(require("ejs"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const paths_1 = require("./paths");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
/**
|
|
14
|
+
* Generate a complete resource: model + controller + views.
|
|
15
|
+
*/
|
|
16
|
+
async function generateResource(name, options = {}) {
|
|
17
|
+
const modelName = (0, utils_1.capitalize)(name);
|
|
18
|
+
const lowerModelName = name.toLowerCase();
|
|
19
|
+
const controllerName = `${modelName}Controller`;
|
|
20
|
+
const resourcePath = (0, utils_1.pluralize)(lowerModelName);
|
|
21
|
+
const tableName = (0, utils_1.pluralize)(lowerModelName);
|
|
22
|
+
console.log(`\nGenerating resource: ${modelName}\n`);
|
|
23
|
+
// Generate model (if database is set up)
|
|
24
|
+
if (!options.skipModel) {
|
|
25
|
+
const schemaPath = path_1.default.resolve("prisma/schema.prisma");
|
|
26
|
+
if (fs_1.default.existsSync(schemaPath)) {
|
|
27
|
+
await generateModel(modelName, tableName, schemaPath, options.skipMigrate);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log("Skipping model (no prisma/schema.prisma found)");
|
|
31
|
+
console.log("Run 'npm run db:setup' first to enable database features\n");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Generate controller
|
|
35
|
+
if (!options.skipController) {
|
|
36
|
+
await generateController(modelName, lowerModelName, controllerName, resourcePath, options.apiOnly);
|
|
37
|
+
}
|
|
38
|
+
// Generate views (unless API only)
|
|
39
|
+
if (!options.skipViews && !options.apiOnly) {
|
|
40
|
+
await generateViews(modelName, lowerModelName, resourcePath);
|
|
41
|
+
}
|
|
42
|
+
// Print summary
|
|
43
|
+
printSummary(modelName, controllerName, resourcePath, options);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate Prisma model.
|
|
47
|
+
*/
|
|
48
|
+
async function generateModel(modelName, tableName, schemaPath, skipMigrate) {
|
|
49
|
+
const templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "model.prisma.ejs");
|
|
50
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
51
|
+
throw new Error("Model template not found");
|
|
52
|
+
}
|
|
53
|
+
// Check if model already exists
|
|
54
|
+
const existingSchema = fs_1.default.readFileSync(schemaPath, "utf-8");
|
|
55
|
+
if (existingSchema.includes(`model ${modelName} {`)) {
|
|
56
|
+
console.log(`Model ${modelName} already exists, skipping...`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const template = fs_1.default.readFileSync(templatePath, "utf-8");
|
|
60
|
+
const modelContent = ejs_1.default.render(template, { modelName, tableName });
|
|
61
|
+
fs_1.default.appendFileSync(schemaPath, "\n" + modelContent);
|
|
62
|
+
console.log(`Created model ${modelName} in prisma/schema.prisma`);
|
|
63
|
+
if (!skipMigrate) {
|
|
64
|
+
console.log("\nRunning Prisma migrate...");
|
|
65
|
+
try {
|
|
66
|
+
(0, child_process_1.execSync)(`npx prisma migrate dev --name add-${tableName}`, {
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
});
|
|
69
|
+
(0, child_process_1.execSync)("npx prisma generate", { stdio: "inherit" });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
console.error("Migration failed. Run manually: npx prisma migrate dev");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate controller file.
|
|
78
|
+
*/
|
|
79
|
+
async function generateController(modelName, lowerModelName, controllerName, resourcePath, apiOnly) {
|
|
80
|
+
const controllersDir = path_1.default.resolve("src/controllers");
|
|
81
|
+
if (!fs_1.default.existsSync(controllersDir)) {
|
|
82
|
+
fs_1.default.mkdirSync(controllersDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const controllerPath = path_1.default.join(controllersDir, `${controllerName}.ts`);
|
|
85
|
+
if (fs_1.default.existsSync(controllerPath)) {
|
|
86
|
+
console.log(`Controller ${controllerName}.ts already exists, skipping...`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Use resource controller template (with full CRUD + form handling)
|
|
90
|
+
const templateName = apiOnly ? "controller.api.ts.ejs" : "controller.resource.ts.ejs";
|
|
91
|
+
let templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), templateName);
|
|
92
|
+
// Fall back to basic controller template if resource template doesn't exist
|
|
93
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
94
|
+
templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "controller.ts.ejs");
|
|
95
|
+
}
|
|
96
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
97
|
+
throw new Error("Controller template not found");
|
|
98
|
+
}
|
|
99
|
+
const template = fs_1.default.readFileSync(templatePath, "utf-8");
|
|
100
|
+
const controllerContent = ejs_1.default.render(template, {
|
|
101
|
+
modelName,
|
|
102
|
+
lowerModelName,
|
|
103
|
+
controllerName,
|
|
104
|
+
resourcePath,
|
|
105
|
+
});
|
|
106
|
+
fs_1.default.writeFileSync(controllerPath, controllerContent);
|
|
107
|
+
console.log(`Created src/controllers/${controllerName}.ts`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generate view files for the resource.
|
|
111
|
+
*/
|
|
112
|
+
async function generateViews(modelName, lowerModelName, resourcePath) {
|
|
113
|
+
const viewsDir = path_1.default.resolve(`src/views/${resourcePath}`);
|
|
114
|
+
if (!fs_1.default.existsSync(viewsDir)) {
|
|
115
|
+
fs_1.default.mkdirSync(viewsDir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
const views = [
|
|
118
|
+
{ name: "index", title: `${modelName} List` },
|
|
119
|
+
{ name: "show", title: `${modelName} Details` },
|
|
120
|
+
{ name: "create", title: `Create ${modelName}` },
|
|
121
|
+
{ name: "edit", title: `Edit ${modelName}` },
|
|
122
|
+
];
|
|
123
|
+
for (const view of views) {
|
|
124
|
+
const viewPath = path_1.default.join(viewsDir, `${view.name}.ejs`);
|
|
125
|
+
if (fs_1.default.existsSync(viewPath)) {
|
|
126
|
+
console.log(`View ${view.name}.ejs already exists, skipping...`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Try specific template first, then fall back to generic
|
|
130
|
+
let templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), `views/${view.name}.ejs.ejs`);
|
|
131
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
132
|
+
templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "view.ejs.ejs");
|
|
133
|
+
}
|
|
134
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
135
|
+
console.warn(`View template for ${view.name} not found, skipping...`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const template = fs_1.default.readFileSync(templatePath, "utf-8");
|
|
139
|
+
const viewContent = ejs_1.default.render(template, {
|
|
140
|
+
title: view.title,
|
|
141
|
+
modelName,
|
|
142
|
+
lowerName: lowerModelName,
|
|
143
|
+
resourcePath,
|
|
144
|
+
viewType: view.name,
|
|
145
|
+
});
|
|
146
|
+
fs_1.default.writeFileSync(viewPath, viewContent);
|
|
147
|
+
}
|
|
148
|
+
console.log(`Created views in src/views/${resourcePath}/`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Print summary of generated files and next steps.
|
|
152
|
+
*/
|
|
153
|
+
function printSummary(modelName, controllerName, resourcePath, options) {
|
|
154
|
+
console.log(`
|
|
155
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
156
|
+
Resource ${modelName} created successfully!
|
|
157
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
158
|
+
|
|
159
|
+
Files created:`);
|
|
160
|
+
if (!options.skipModel) {
|
|
161
|
+
console.log(` prisma/schema.prisma (${modelName} model added)`);
|
|
162
|
+
}
|
|
163
|
+
if (!options.skipController) {
|
|
164
|
+
console.log(` src/controllers/${controllerName}.ts`);
|
|
165
|
+
}
|
|
166
|
+
if (!options.skipViews && !options.apiOnly) {
|
|
167
|
+
console.log(` src/views/${resourcePath}/index.ejs`);
|
|
168
|
+
console.log(` src/views/${resourcePath}/show.ejs`);
|
|
169
|
+
console.log(` src/views/${resourcePath}/create.ejs`);
|
|
170
|
+
console.log(` src/views/${resourcePath}/edit.ejs`);
|
|
171
|
+
}
|
|
172
|
+
console.log(`
|
|
173
|
+
Routes:
|
|
174
|
+
GET /${resourcePath} → List all ${resourcePath}
|
|
175
|
+
GET /${resourcePath}/create → Show create form
|
|
176
|
+
POST /${resourcePath} → Create new ${modelName.toLowerCase()}
|
|
177
|
+
GET /${resourcePath}/:id → Show ${modelName.toLowerCase()}
|
|
178
|
+
GET /${resourcePath}/:id/edit → Show edit form
|
|
179
|
+
PUT /${resourcePath}/:id → Update ${modelName.toLowerCase()}
|
|
180
|
+
DELETE /${resourcePath}/:id → Delete ${modelName.toLowerCase()}
|
|
181
|
+
|
|
182
|
+
Next steps:
|
|
183
|
+
1. Add routes to src/server.ts:
|
|
184
|
+
|
|
185
|
+
import * as ${controllerName} from "./controllers/${controllerName}";
|
|
186
|
+
|
|
187
|
+
app.get("/${resourcePath}", ${controllerName}.index);
|
|
188
|
+
app.get("/${resourcePath}/create", ${controllerName}.create);
|
|
189
|
+
app.post("/${resourcePath}", ${controllerName}.store);
|
|
190
|
+
app.get("/${resourcePath}/:id", ${controllerName}.show);
|
|
191
|
+
app.get("/${resourcePath}/:id/edit", ${controllerName}.edit);
|
|
192
|
+
app.put("/${resourcePath}/:id", ${controllerName}.update);
|
|
193
|
+
app.delete("/${resourcePath}/:id", ${controllerName}.destroy);
|
|
194
|
+
|
|
195
|
+
2. Edit the model in prisma/schema.prisma to add your fields
|
|
196
|
+
3. Run: npx prisma migrate dev --name add-${resourcePath}-fields
|
|
197
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
198
|
+
`);
|
|
199
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for generators.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Capitalize the first letter of a string.
|
|
6
|
+
*/
|
|
7
|
+
export declare function capitalize(str: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Pluralize a word with common English rules.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pluralize(str: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Convert to kebab-case (for URLs).
|
|
14
|
+
*/
|
|
15
|
+
export declare function kebabCase(str: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Convert to snake_case (for database tables).
|
|
18
|
+
*/
|
|
19
|
+
export declare function snakeCase(str: string): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for generators.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.capitalize = capitalize;
|
|
7
|
+
exports.pluralize = pluralize;
|
|
8
|
+
exports.kebabCase = kebabCase;
|
|
9
|
+
exports.snakeCase = snakeCase;
|
|
10
|
+
/**
|
|
11
|
+
* Capitalize the first letter of a string.
|
|
12
|
+
*/
|
|
13
|
+
function capitalize(str) {
|
|
14
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pluralize a word with common English rules.
|
|
18
|
+
*/
|
|
19
|
+
function pluralize(str) {
|
|
20
|
+
const lower = str.toLowerCase();
|
|
21
|
+
// Irregular plurals
|
|
22
|
+
const irregulars = {
|
|
23
|
+
person: "people",
|
|
24
|
+
child: "children",
|
|
25
|
+
man: "men",
|
|
26
|
+
woman: "women",
|
|
27
|
+
tooth: "teeth",
|
|
28
|
+
foot: "feet",
|
|
29
|
+
mouse: "mice",
|
|
30
|
+
goose: "geese",
|
|
31
|
+
};
|
|
32
|
+
if (irregulars[lower]) {
|
|
33
|
+
// Preserve original casing
|
|
34
|
+
return str.charAt(0) === str.charAt(0).toUpperCase()
|
|
35
|
+
? capitalize(irregulars[lower])
|
|
36
|
+
: irregulars[lower];
|
|
37
|
+
}
|
|
38
|
+
// Already plural or uncountable
|
|
39
|
+
if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z")) {
|
|
40
|
+
if (lower.endsWith("ss") || lower.endsWith("us") || lower.endsWith("is")) {
|
|
41
|
+
return str + "es";
|
|
42
|
+
}
|
|
43
|
+
return str;
|
|
44
|
+
}
|
|
45
|
+
// Words ending in consonant + y
|
|
46
|
+
if (lower.endsWith("y") && !/[aeiou]y$/.test(lower)) {
|
|
47
|
+
return str.slice(0, -1) + "ies";
|
|
48
|
+
}
|
|
49
|
+
// Words ending in ch, sh, x, z, s
|
|
50
|
+
if (/(?:ch|sh|x|z|s)$/.test(lower)) {
|
|
51
|
+
return str + "es";
|
|
52
|
+
}
|
|
53
|
+
// Words ending in f or fe
|
|
54
|
+
if (lower.endsWith("f")) {
|
|
55
|
+
return str.slice(0, -1) + "ves";
|
|
56
|
+
}
|
|
57
|
+
if (lower.endsWith("fe")) {
|
|
58
|
+
return str.slice(0, -2) + "ves";
|
|
59
|
+
}
|
|
60
|
+
// Default: add s
|
|
61
|
+
return str + "s";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert to kebab-case (for URLs).
|
|
65
|
+
*/
|
|
66
|
+
function kebabCase(str) {
|
|
67
|
+
return str
|
|
68
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
69
|
+
.replace(/[\s_]+/g, "-")
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert to snake_case (for database tables).
|
|
74
|
+
*/
|
|
75
|
+
function snakeCase(str) {
|
|
76
|
+
return str
|
|
77
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
78
|
+
.replace(/[\s-]+/g, "_")
|
|
79
|
+
.toLowerCase();
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { getPrismaClient } from "@erwininteractive/mvc";
|
|
3
|
+
|
|
4
|
+
const prisma = getPrismaClient();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /<%= resourcePath %>
|
|
8
|
+
* List all <%= lowerModelName %> records.
|
|
9
|
+
*/
|
|
10
|
+
export async function index(req: Request, res: Response): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const items = await prisma.<%= lowerModelName %>.findMany({
|
|
13
|
+
orderBy: { createdAt: "desc" },
|
|
14
|
+
});
|
|
15
|
+
res.render("<%= resourcePath %>/index", {
|
|
16
|
+
title: "<%= modelName %> List",
|
|
17
|
+
items,
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Error fetching <%= resourcePath %>:", error);
|
|
21
|
+
res.status(500).render("error", {
|
|
22
|
+
title: "Error",
|
|
23
|
+
message: "Failed to load <%= resourcePath %>",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET /<%= resourcePath %>/create
|
|
30
|
+
* Show form to create a new <%= lowerModelName %>.
|
|
31
|
+
*/
|
|
32
|
+
export async function create(req: Request, res: Response): Promise<void> {
|
|
33
|
+
res.render("<%= resourcePath %>/create", {
|
|
34
|
+
title: "Create <%= modelName %>",
|
|
35
|
+
item: {},
|
|
36
|
+
errors: {},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* POST /<%= resourcePath %>
|
|
42
|
+
* Create a new <%= lowerModelName %> record.
|
|
43
|
+
*/
|
|
44
|
+
export async function store(req: Request, res: Response): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
const data = req.body;
|
|
47
|
+
const item = await prisma.<%= lowerModelName %>.create({ data });
|
|
48
|
+
res.redirect(`/<%= resourcePath %>/${item.id}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Error creating <%= lowerModelName %>:", error);
|
|
51
|
+
res.status(400).render("<%= resourcePath %>/create", {
|
|
52
|
+
title: "Create <%= modelName %>",
|
|
53
|
+
item: req.body,
|
|
54
|
+
errors: { _form: "Failed to create <%= lowerModelName %>. Please check your input." },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* GET /<%= resourcePath %>/:id
|
|
61
|
+
* Show a single <%= lowerModelName %> record.
|
|
62
|
+
*/
|
|
63
|
+
export async function show(req: Request, res: Response): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const id = Number(req.params.id);
|
|
66
|
+
if (isNaN(id)) {
|
|
67
|
+
res.status(400).render("error", { title: "Error", message: "Invalid ID" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const item = await prisma.<%= lowerModelName %>.findUnique({ where: { id } });
|
|
72
|
+
if (!item) {
|
|
73
|
+
res.status(404).render("error", {
|
|
74
|
+
title: "Not Found",
|
|
75
|
+
message: "<%= modelName %> not found",
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
res.render("<%= resourcePath %>/show", {
|
|
81
|
+
title: "<%= modelName %> Details",
|
|
82
|
+
item,
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error("Error fetching <%= lowerModelName %>:", error);
|
|
86
|
+
res.status(500).render("error", {
|
|
87
|
+
title: "Error",
|
|
88
|
+
message: "Failed to load <%= lowerModelName %>",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GET /<%= resourcePath %>/:id/edit
|
|
95
|
+
* Show form to edit a <%= lowerModelName %>.
|
|
96
|
+
*/
|
|
97
|
+
export async function edit(req: Request, res: Response): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const id = Number(req.params.id);
|
|
100
|
+
if (isNaN(id)) {
|
|
101
|
+
res.status(400).render("error", { title: "Error", message: "Invalid ID" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const item = await prisma.<%= lowerModelName %>.findUnique({ where: { id } });
|
|
106
|
+
if (!item) {
|
|
107
|
+
res.status(404).render("error", {
|
|
108
|
+
title: "Not Found",
|
|
109
|
+
message: "<%= modelName %> not found",
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.render("<%= resourcePath %>/edit", {
|
|
115
|
+
title: "Edit <%= modelName %>",
|
|
116
|
+
item,
|
|
117
|
+
errors: {},
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("Error fetching <%= lowerModelName %>:", error);
|
|
121
|
+
res.status(500).render("error", {
|
|
122
|
+
title: "Error",
|
|
123
|
+
message: "Failed to load <%= lowerModelName %>",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* PUT /<%= resourcePath %>/:id
|
|
130
|
+
* Update a <%= lowerModelName %> record.
|
|
131
|
+
*/
|
|
132
|
+
export async function update(req: Request, res: Response): Promise<void> {
|
|
133
|
+
try {
|
|
134
|
+
const id = Number(req.params.id);
|
|
135
|
+
if (isNaN(id)) {
|
|
136
|
+
res.status(400).render("error", { title: "Error", message: "Invalid ID" });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = req.body;
|
|
141
|
+
await prisma.<%= lowerModelName %>.update({
|
|
142
|
+
where: { id },
|
|
143
|
+
data,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
res.redirect(`/<%= resourcePath %>/${id}`);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error("Error updating <%= lowerModelName %>:", error);
|
|
149
|
+
res.status(400).render("<%= resourcePath %>/edit", {
|
|
150
|
+
title: "Edit <%= modelName %>",
|
|
151
|
+
item: { id: req.params.id, ...req.body },
|
|
152
|
+
errors: { _form: "Failed to update <%= lowerModelName %>. Please check your input." },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* DELETE /<%= resourcePath %>/:id
|
|
159
|
+
* Delete a <%= lowerModelName %> record.
|
|
160
|
+
*/
|
|
161
|
+
export async function destroy(req: Request, res: Response): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
const id = Number(req.params.id);
|
|
164
|
+
if (isNaN(id)) {
|
|
165
|
+
res.status(400).json({ error: "Invalid ID" });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await prisma.<%= lowerModelName %>.delete({ where: { id } });
|
|
170
|
+
|
|
171
|
+
// Check if request wants JSON response
|
|
172
|
+
if (req.accepts("json") && !req.accepts("html")) {
|
|
173
|
+
res.json({ success: true });
|
|
174
|
+
} else {
|
|
175
|
+
res.redirect("/<%= resourcePath %>");
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Error deleting <%= lowerModelName %>:", error);
|
|
179
|
+
res.status(500).json({ error: "Failed to delete <%= lowerModelName %>" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%%= title %></title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
max-width: 600px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
background: #f5f5f5;
|
|
16
|
+
}
|
|
17
|
+
h1 { color: #333; margin-bottom: 1rem; }
|
|
18
|
+
.breadcrumb {
|
|
19
|
+
margin-bottom: 1.5rem;
|
|
20
|
+
color: #666;
|
|
21
|
+
}
|
|
22
|
+
.breadcrumb a { color: #667eea; text-decoration: none; }
|
|
23
|
+
.breadcrumb a:hover { text-decoration: underline; }
|
|
24
|
+
.card {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
}
|
|
30
|
+
.form-group { margin-bottom: 1.5rem; }
|
|
31
|
+
label {
|
|
32
|
+
display: block;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
margin-bottom: 0.5rem;
|
|
35
|
+
color: #333;
|
|
36
|
+
}
|
|
37
|
+
input[type="text"],
|
|
38
|
+
input[type="email"],
|
|
39
|
+
input[type="number"],
|
|
40
|
+
input[type="date"],
|
|
41
|
+
textarea,
|
|
42
|
+
select {
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: 0.75rem;
|
|
45
|
+
border: 1px solid #ddd;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
font-size: 1rem;
|
|
48
|
+
transition: border-color 0.2s;
|
|
49
|
+
}
|
|
50
|
+
input:focus, textarea:focus, select:focus {
|
|
51
|
+
outline: none;
|
|
52
|
+
border-color: #667eea;
|
|
53
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
54
|
+
}
|
|
55
|
+
textarea { min-height: 120px; resize: vertical; }
|
|
56
|
+
.error-message {
|
|
57
|
+
color: #dc3545;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
margin-top: 0.25rem;
|
|
60
|
+
}
|
|
61
|
+
.form-error {
|
|
62
|
+
background: #f8d7da;
|
|
63
|
+
color: #721c24;
|
|
64
|
+
padding: 1rem;
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
margin-bottom: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
.actions {
|
|
69
|
+
display: flex;
|
|
70
|
+
gap: 0.75rem;
|
|
71
|
+
margin-top: 1.5rem;
|
|
72
|
+
padding-top: 1.5rem;
|
|
73
|
+
border-top: 1px solid #eee;
|
|
74
|
+
}
|
|
75
|
+
.btn {
|
|
76
|
+
display: inline-block;
|
|
77
|
+
padding: 0.75rem 1.5rem;
|
|
78
|
+
background: #667eea;
|
|
79
|
+
color: white;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
border-radius: 6px;
|
|
82
|
+
border: none;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
font-size: 1rem;
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
}
|
|
87
|
+
.btn:hover { background: #5a6fd6; }
|
|
88
|
+
.btn-secondary { background: #6c757d; }
|
|
89
|
+
.btn-secondary:hover { background: #5a6268; }
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<div class="breadcrumb">
|
|
94
|
+
<a href="/<%= resourcePath %>"><%= modelName %> List</a> / Create New
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<h1><%%= title %></h1>
|
|
98
|
+
|
|
99
|
+
<div class="card">
|
|
100
|
+
<%% if (errors && errors._form) { %>
|
|
101
|
+
<div class="form-error"><%%= errors._form %></div>
|
|
102
|
+
<%% } %>
|
|
103
|
+
|
|
104
|
+
<form method="POST" action="/<%= resourcePath %>">
|
|
105
|
+
<!--
|
|
106
|
+
Add your form fields here. Example:
|
|
107
|
+
|
|
108
|
+
<div class="form-group">
|
|
109
|
+
<label for="title">Title</label>
|
|
110
|
+
<input type="text" id="title" name="title" value="<%%- item.title || '' %>" required>
|
|
111
|
+
<%% if (errors && errors.title) { %>
|
|
112
|
+
<div class="error-message"><%%= errors.title %></div>
|
|
113
|
+
<%% } %>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="form-group">
|
|
117
|
+
<label for="description">Description</label>
|
|
118
|
+
<textarea id="description" name="description"><%%- item.description || '' %></textarea>
|
|
119
|
+
</div>
|
|
120
|
+
-->
|
|
121
|
+
|
|
122
|
+
<div class="form-group">
|
|
123
|
+
<label for="placeholder">Placeholder Field</label>
|
|
124
|
+
<input type="text" id="placeholder" name="placeholder" placeholder="Edit this form to add your fields">
|
|
125
|
+
<p style="color: #666; font-size: 0.875rem; margin-top: 0.5rem;">
|
|
126
|
+
Edit this template at <code>src/views/<%= resourcePath %>/create.ejs</code> to add your model fields.
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="actions">
|
|
131
|
+
<button type="submit" class="btn">Create <%= modelName %></button>
|
|
132
|
+
<a href="/<%= resourcePath %>" class="btn btn-secondary">Cancel</a>
|
|
133
|
+
</div>
|
|
134
|
+
</form>
|
|
135
|
+
</div>
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%%= title %></title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
max-width: 600px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
background: #f5f5f5;
|
|
16
|
+
}
|
|
17
|
+
h1 { color: #333; margin-bottom: 1rem; }
|
|
18
|
+
.breadcrumb {
|
|
19
|
+
margin-bottom: 1.5rem;
|
|
20
|
+
color: #666;
|
|
21
|
+
}
|
|
22
|
+
.breadcrumb a { color: #667eea; text-decoration: none; }
|
|
23
|
+
.breadcrumb a:hover { text-decoration: underline; }
|
|
24
|
+
.card {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
}
|
|
30
|
+
.form-group { margin-bottom: 1.5rem; }
|
|
31
|
+
label {
|
|
32
|
+
display: block;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
margin-bottom: 0.5rem;
|
|
35
|
+
color: #333;
|
|
36
|
+
}
|
|
37
|
+
input[type="text"],
|
|
38
|
+
input[type="email"],
|
|
39
|
+
input[type="number"],
|
|
40
|
+
input[type="date"],
|
|
41
|
+
textarea,
|
|
42
|
+
select {
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: 0.75rem;
|
|
45
|
+
border: 1px solid #ddd;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
font-size: 1rem;
|
|
48
|
+
transition: border-color 0.2s;
|
|
49
|
+
}
|
|
50
|
+
input:focus, textarea:focus, select:focus {
|
|
51
|
+
outline: none;
|
|
52
|
+
border-color: #667eea;
|
|
53
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
54
|
+
}
|
|
55
|
+
textarea { min-height: 120px; resize: vertical; }
|
|
56
|
+
.error-message {
|
|
57
|
+
color: #dc3545;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
margin-top: 0.25rem;
|
|
60
|
+
}
|
|
61
|
+
.form-error {
|
|
62
|
+
background: #f8d7da;
|
|
63
|
+
color: #721c24;
|
|
64
|
+
padding: 1rem;
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
margin-bottom: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
.meta {
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
color: #666;
|
|
71
|
+
margin-bottom: 1.5rem;
|
|
72
|
+
padding-bottom: 1rem;
|
|
73
|
+
border-bottom: 1px solid #eee;
|
|
74
|
+
}
|
|
75
|
+
.actions {
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 0.75rem;
|
|
78
|
+
margin-top: 1.5rem;
|
|
79
|
+
padding-top: 1.5rem;
|
|
80
|
+
border-top: 1px solid #eee;
|
|
81
|
+
}
|
|
82
|
+
.btn {
|
|
83
|
+
display: inline-block;
|
|
84
|
+
padding: 0.75rem 1.5rem;
|
|
85
|
+
background: #667eea;
|
|
86
|
+
color: white;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
border-radius: 6px;
|
|
89
|
+
border: none;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
font-size: 1rem;
|
|
92
|
+
font-weight: 500;
|
|
93
|
+
}
|
|
94
|
+
.btn:hover { background: #5a6fd6; }
|
|
95
|
+
.btn-secondary { background: #6c757d; }
|
|
96
|
+
.btn-secondary:hover { background: #5a6268; }
|
|
97
|
+
.btn-danger { background: #dc3545; }
|
|
98
|
+
.btn-danger:hover { background: #c82333; }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div class="breadcrumb">
|
|
103
|
+
<a href="/<%= resourcePath %>"><%= modelName %> List</a> /
|
|
104
|
+
<a href="/<%= resourcePath %>/<%%= item.id %>"><%%= item.id %></a> / Edit
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<h1><%%= title %></h1>
|
|
108
|
+
|
|
109
|
+
<div class="card">
|
|
110
|
+
<div class="meta">
|
|
111
|
+
Editing <%= modelName %> #<%%= item.id %>
|
|
112
|
+
<%% if (item.updatedAt) { %>
|
|
113
|
+
· Last updated: <%%= new Date(item.updatedAt).toLocaleString() %>
|
|
114
|
+
<%% } %>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<%% if (errors && errors._form) { %>
|
|
118
|
+
<div class="form-error"><%%= errors._form %></div>
|
|
119
|
+
<%% } %>
|
|
120
|
+
|
|
121
|
+
<form method="POST" action="/<%= resourcePath %>/<%%= item.id %>?_method=PUT">
|
|
122
|
+
<!--
|
|
123
|
+
Add your form fields here. Example:
|
|
124
|
+
|
|
125
|
+
<div class="form-group">
|
|
126
|
+
<label for="title">Title</label>
|
|
127
|
+
<input type="text" id="title" name="title" value="<%%- item.title || '' %>" required>
|
|
128
|
+
<%% if (errors && errors.title) { %>
|
|
129
|
+
<div class="error-message"><%%= errors.title %></div>
|
|
130
|
+
<%% } %>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="form-group">
|
|
134
|
+
<label for="description">Description</label>
|
|
135
|
+
<textarea id="description" name="description"><%%- item.description || '' %></textarea>
|
|
136
|
+
</div>
|
|
137
|
+
-->
|
|
138
|
+
|
|
139
|
+
<div class="form-group">
|
|
140
|
+
<label for="placeholder">Placeholder Field</label>
|
|
141
|
+
<input type="text" id="placeholder" name="placeholder" placeholder="Edit this form to add your fields">
|
|
142
|
+
<p style="color: #666; font-size: 0.875rem; margin-top: 0.5rem;">
|
|
143
|
+
Edit this template at <code>src/views/<%= resourcePath %>/edit.ejs</code> to add your model fields.
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="actions">
|
|
148
|
+
<button type="submit" class="btn">Update <%= modelName %></button>
|
|
149
|
+
<a href="/<%= resourcePath %>/<%%= item.id %>" class="btn btn-secondary">Cancel</a>
|
|
150
|
+
<form method="POST" action="/<%= resourcePath %>/<%%= item.id %>?_method=DELETE" style="display:inline; margin-left: auto;">
|
|
151
|
+
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this <%= lowerName %>?')">Delete</button>
|
|
152
|
+
</form>
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
</div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%%= title %></title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
max-width: 900px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
background: #f5f5f5;
|
|
16
|
+
}
|
|
17
|
+
h1 { color: #333; margin-bottom: 1rem; }
|
|
18
|
+
.actions { margin-bottom: 1.5rem; }
|
|
19
|
+
.btn {
|
|
20
|
+
display: inline-block;
|
|
21
|
+
padding: 0.5rem 1rem;
|
|
22
|
+
background: #667eea;
|
|
23
|
+
color: white;
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
border: none;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
font-size: 1rem;
|
|
29
|
+
}
|
|
30
|
+
.btn:hover { background: #5a6fd6; }
|
|
31
|
+
.btn-danger { background: #dc3545; }
|
|
32
|
+
.btn-danger:hover { background: #c82333; }
|
|
33
|
+
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
|
|
34
|
+
table {
|
|
35
|
+
width: 100%;
|
|
36
|
+
border-collapse: collapse;
|
|
37
|
+
background: white;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
41
|
+
}
|
|
42
|
+
th, td {
|
|
43
|
+
padding: 1rem;
|
|
44
|
+
text-align: left;
|
|
45
|
+
border-bottom: 1px solid #eee;
|
|
46
|
+
}
|
|
47
|
+
th { background: #f8f9fa; font-weight: 600; }
|
|
48
|
+
tr:hover { background: #f8f9fa; }
|
|
49
|
+
.empty {
|
|
50
|
+
text-align: center;
|
|
51
|
+
padding: 3rem;
|
|
52
|
+
color: #666;
|
|
53
|
+
}
|
|
54
|
+
.item-actions { white-space: nowrap; }
|
|
55
|
+
.item-actions a, .item-actions button { margin-right: 0.5rem; }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<h1><%%= title %></h1>
|
|
60
|
+
|
|
61
|
+
<div class="actions">
|
|
62
|
+
<a href="/<%= resourcePath %>/create" class="btn">Create New <%= modelName %></a>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<%% if (items && items.length > 0) { %>
|
|
66
|
+
<table>
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>ID</th>
|
|
70
|
+
<th>Created</th>
|
|
71
|
+
<th>Actions</th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
<%% items.forEach(item => { %>
|
|
76
|
+
<tr>
|
|
77
|
+
<td><%%= item.id %></td>
|
|
78
|
+
<td><%%= new Date(item.createdAt).toLocaleDateString() %></td>
|
|
79
|
+
<td class="item-actions">
|
|
80
|
+
<a href="/<%= resourcePath %>/<%%= item.id %>" class="btn btn-sm">View</a>
|
|
81
|
+
<a href="/<%= resourcePath %>/<%%= item.id %>/edit" class="btn btn-sm">Edit</a>
|
|
82
|
+
<form method="POST" action="/<%= resourcePath %>/<%%= item.id %>?_method=DELETE" style="display:inline">
|
|
83
|
+
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
|
84
|
+
</form>
|
|
85
|
+
</td>
|
|
86
|
+
</tr>
|
|
87
|
+
<%% }); %>
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
<%% } else { %>
|
|
91
|
+
<div class="empty">
|
|
92
|
+
<p>No <%= resourcePath %> found.</p>
|
|
93
|
+
<a href="/<%= resourcePath %>/create" class="btn">Create your first <%= lowerName %></a>
|
|
94
|
+
</div>
|
|
95
|
+
<%% } %>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%%= title %></title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
max-width: 700px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
background: #f5f5f5;
|
|
16
|
+
}
|
|
17
|
+
h1 { color: #333; margin-bottom: 1rem; }
|
|
18
|
+
.breadcrumb {
|
|
19
|
+
margin-bottom: 1.5rem;
|
|
20
|
+
color: #666;
|
|
21
|
+
}
|
|
22
|
+
.breadcrumb a { color: #667eea; text-decoration: none; }
|
|
23
|
+
.breadcrumb a:hover { text-decoration: underline; }
|
|
24
|
+
.card {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
margin-bottom: 1.5rem;
|
|
30
|
+
}
|
|
31
|
+
.field { margin-bottom: 1.5rem; }
|
|
32
|
+
.field:last-child { margin-bottom: 0; }
|
|
33
|
+
.field-label {
|
|
34
|
+
font-size: 0.875rem;
|
|
35
|
+
color: #666;
|
|
36
|
+
margin-bottom: 0.25rem;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
letter-spacing: 0.05em;
|
|
39
|
+
}
|
|
40
|
+
.field-value {
|
|
41
|
+
font-size: 1.1rem;
|
|
42
|
+
color: #333;
|
|
43
|
+
}
|
|
44
|
+
.actions { display: flex; gap: 0.75rem; }
|
|
45
|
+
.btn {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
padding: 0.5rem 1rem;
|
|
48
|
+
background: #667eea;
|
|
49
|
+
color: white;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
border: none;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font-size: 1rem;
|
|
55
|
+
}
|
|
56
|
+
.btn:hover { background: #5a6fd6; }
|
|
57
|
+
.btn-secondary { background: #6c757d; }
|
|
58
|
+
.btn-secondary:hover { background: #5a6268; }
|
|
59
|
+
.btn-danger { background: #dc3545; }
|
|
60
|
+
.btn-danger:hover { background: #c82333; }
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="breadcrumb">
|
|
65
|
+
<a href="/<%= resourcePath %>"><%= modelName %> List</a> / Details
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<h1><%%= title %></h1>
|
|
69
|
+
|
|
70
|
+
<div class="card">
|
|
71
|
+
<div class="field">
|
|
72
|
+
<div class="field-label">ID</div>
|
|
73
|
+
<div class="field-value"><%%= item.id %></div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="field">
|
|
77
|
+
<div class="field-label">Created At</div>
|
|
78
|
+
<div class="field-value"><%%= new Date(item.createdAt).toLocaleString() %></div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="field">
|
|
82
|
+
<div class="field-label">Updated At</div>
|
|
83
|
+
<div class="field-value"><%%= new Date(item.updatedAt).toLocaleString() %></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Add more fields here as you add them to your model -->
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="actions">
|
|
90
|
+
<a href="/<%= resourcePath %>/<%%= item.id %>/edit" class="btn">Edit</a>
|
|
91
|
+
<a href="/<%= resourcePath %>" class="btn btn-secondary">Back to List</a>
|
|
92
|
+
<form method="POST" action="/<%= resourcePath %>/<%%= item.id %>?_method=DELETE" style="display:inline">
|
|
93
|
+
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this <%= lowerName %>?')">Delete</button>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|