create-diolo-app 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/index.js +152 -0
- package/package.json +39 -0
- package/templates/banking-management/template.config.json +1 -0
- package/templates/bot-starter/template.config.json +1 -0
- package/templates/car-management/template.config.json +1 -0
- package/templates/ecommerce-system/template.config.json +1 -0
- package/templates/employee-management/template.config.json +1 -0
- package/templates/express-api/template.config.json +1 -0
- package/templates/hospital-management/template.config.json +1 -0
- package/templates/hotel-management/template.config.json +1 -0
- package/templates/inventory-management/template.config.json +1 -0
- package/templates/library-management/template.config.json +1 -0
- package/templates/pharmacy-management/template.config.json +1 -0
- package/templates/pos-system/template.config.json +1 -0
- package/templates/react-dashboard/template.config.json +1 -0
- package/templates/sales-management/template.config.json +1 -0
- package/templates/school-management/template.config.json +1 -0
- package/templates/slot-car-management/template.config.json +1 -0
- package/templates/stock-management/template.config.json +1 -0
- package/templates/supply-chain/template.config.json +1 -0
- package/utils/createEnv.js +37 -0
- package/utils/generateProjectFiles.js +841 -0
- package/utils/gitInit.js +23 -0
- package/utils/installDependencies.js +39 -0
- package/utils/replaceVariables.js +38 -0
package/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import { installDependencies } from "./utils/installDependencies.js";
|
|
10
|
+
import { createEnvFiles } from "./utils/createEnv.js";
|
|
11
|
+
import { replaceVariables } from "./utils/replaceVariables.js";
|
|
12
|
+
import { gitInit } from "./utils/gitInit.js";
|
|
13
|
+
import { generateProjectFiles } from "./utils/generateProjectFiles.js";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const templates = [
|
|
19
|
+
{ name: "Employee Management System", value: "employee-management" },
|
|
20
|
+
{ name: "Stock Management System", value: "stock-management" },
|
|
21
|
+
{ name: "Car Management System", value: "car-management" },
|
|
22
|
+
{ name: "Slot Car Management System", value: "slot-car-management" },
|
|
23
|
+
{ name: "Inventory Management System", value: "inventory-management" },
|
|
24
|
+
{ name: "Sales Records Management System", value: "sales-management" },
|
|
25
|
+
{ name: "Supply Chain Management System", value: "supply-chain" },
|
|
26
|
+
{ name: "School Management System", value: "school-management" },
|
|
27
|
+
{ name: "Hospital Management System", value: "hospital-management" },
|
|
28
|
+
{ name: "Hotel Management System", value: "hotel-management" },
|
|
29
|
+
{ name: "POS System", value: "pos-system" },
|
|
30
|
+
{ name: "E-Commerce System", value: "ecommerce-system" },
|
|
31
|
+
{ name: "Library Management System", value: "library-management" },
|
|
32
|
+
{ name: "Pharmacy Management System", value: "pharmacy-management" },
|
|
33
|
+
{ name: "Banking Management System", value: "banking-management" },
|
|
34
|
+
{ name: "Express API Starter", value: "express-api" },
|
|
35
|
+
{ name: "React Dashboard Starter", value: "react-dashboard" },
|
|
36
|
+
{ name: "Node.js Bot Starter", value: "bot-starter" }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function slugify(value) {
|
|
40
|
+
return value
|
|
41
|
+
.trim()
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
+
.replace(/(^-|-$)+/g, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function databaseName(projectName) {
|
|
48
|
+
return slugify(projectName).replace(/-/g, "_");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
console.log(chalk.cyan.bold("\n🚀 Welcome to Create Diolo App\n"));
|
|
53
|
+
|
|
54
|
+
const answers = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: "input",
|
|
57
|
+
name: "projectName",
|
|
58
|
+
message: "Project Name",
|
|
59
|
+
default: "diolo-business-app",
|
|
60
|
+
filter: slugify,
|
|
61
|
+
validate: (value) => (value ? true : "Project name is required.")
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "list",
|
|
65
|
+
name: "template",
|
|
66
|
+
message: "Choose Template",
|
|
67
|
+
choices: templates
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: "list",
|
|
71
|
+
name: "databaseType",
|
|
72
|
+
message: "Database Type?",
|
|
73
|
+
choices: ["MySQL", "PostgreSQL", "MongoDB"]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "list",
|
|
77
|
+
name: "packageManager",
|
|
78
|
+
message: "Package Manager?",
|
|
79
|
+
choices: ["npm", "pnpm", "yarn"]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "input",
|
|
83
|
+
name: "port",
|
|
84
|
+
message: "Backend Port",
|
|
85
|
+
default: "5000",
|
|
86
|
+
validate: (value) => (/^\d+$/.test(value) ? true : "Port must be a number.")
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const targetDir = path.resolve(process.cwd(), answers.projectName);
|
|
91
|
+
const templateDir = path.join(__dirname, "templates", answers.template);
|
|
92
|
+
const templateConfigPath = path.join(templateDir, "template.config.json");
|
|
93
|
+
|
|
94
|
+
if (await fs.pathExists(targetDir)) {
|
|
95
|
+
const { overwrite } = await inquirer.prompt([
|
|
96
|
+
{
|
|
97
|
+
type: "confirm",
|
|
98
|
+
name: "overwrite",
|
|
99
|
+
message: `${answers.projectName} already exists. Overwrite it?`,
|
|
100
|
+
default: false
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
if (!overwrite) {
|
|
105
|
+
console.log(chalk.yellow("Project creation cancelled."));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await fs.remove(targetDir);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const spinner = ora("Creating project files...").start();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fs.ensureDir(targetDir);
|
|
116
|
+
await fs.copy(templateDir, targetDir);
|
|
117
|
+
|
|
118
|
+
const templateConfig = await fs.readJson(templateConfigPath);
|
|
119
|
+
const variables = {
|
|
120
|
+
PROJECT_NAME: answers.projectName,
|
|
121
|
+
DATABASE_NAME: databaseName(answers.projectName),
|
|
122
|
+
DATABASE_TYPE: answers.databaseType,
|
|
123
|
+
PORT: answers.port,
|
|
124
|
+
TEMPLATE_NAME: templateConfig.name
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
await generateProjectFiles(targetDir, {
|
|
128
|
+
...answers,
|
|
129
|
+
databaseName: variables.DATABASE_NAME,
|
|
130
|
+
templateConfig
|
|
131
|
+
});
|
|
132
|
+
await createEnvFiles(targetDir, variables, answers.databaseType);
|
|
133
|
+
await replaceVariables(targetDir, variables);
|
|
134
|
+
|
|
135
|
+
spinner.succeed("Project files created.");
|
|
136
|
+
|
|
137
|
+
await installDependencies(targetDir, answers.packageManager);
|
|
138
|
+
await gitInit(targetDir);
|
|
139
|
+
|
|
140
|
+
console.log(chalk.green.bold("\n✅ Project Created Successfully\n"));
|
|
141
|
+
console.log(chalk.bold("Next Steps:\n"));
|
|
142
|
+
console.log(`cd ${answers.projectName}`);
|
|
143
|
+
console.log(`${answers.packageManager} install`);
|
|
144
|
+
console.log(`${answers.packageManager} run dev\n`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
spinner.fail("Project creation failed.");
|
|
147
|
+
console.error(chalk.red(error.message));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-diolo-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create production-ready Diolo full-stack business management applications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-diolo-app": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"utils",
|
|
13
|
+
"templates"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node index.js",
|
|
17
|
+
"lint": "node --check index.js && node --check utils/*.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"create",
|
|
21
|
+
"cli",
|
|
22
|
+
"react",
|
|
23
|
+
"vite",
|
|
24
|
+
"express",
|
|
25
|
+
"business",
|
|
26
|
+
"dashboard"
|
|
27
|
+
],
|
|
28
|
+
"author": "Diolo",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^5.3.0",
|
|
32
|
+
"fs-extra": "^11.2.0",
|
|
33
|
+
"inquirer": "^9.2.23",
|
|
34
|
+
"ora": "^8.1.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.18.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Banking Management System", "slug": "banking-management", "primaryEntity": "Account" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Node.js Bot Starter", "slug": "bot-starter", "primaryEntity": "Task" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Car Management System", "slug": "car-management", "primaryEntity": "Car" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "E-Commerce System", "slug": "ecommerce-system", "primaryEntity": "Order" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Employee Management System", "slug": "employee-management", "primaryEntity": "Employee" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Express API Starter", "slug": "express-api", "primaryEntity": "Record" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Hospital Management System", "slug": "hospital-management", "primaryEntity": "Patient" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Hotel Management System", "slug": "hotel-management", "primaryEntity": "Booking" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Inventory Management System", "slug": "inventory-management", "primaryEntity": "Inventory Item" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Library Management System", "slug": "library-management", "primaryEntity": "Book" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Pharmacy Management System", "slug": "pharmacy-management", "primaryEntity": "Medicine" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "POS System", "slug": "pos-system", "primaryEntity": "Transaction" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "React Dashboard Starter", "slug": "react-dashboard", "primaryEntity": "Dashboard" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Sales Records Management System", "slug": "sales-management", "primaryEntity": "Sale" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "School Management System", "slug": "school-management", "primaryEntity": "Student" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Slot Car Management System", "slug": "slot-car-management", "primaryEntity": "Slot Car" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Stock Management System", "slug": "stock-management", "primaryEntity": "Stock Item" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Supply Chain Management System", "slug": "supply-chain", "primaryEntity": "Shipment" }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
function dbDriver(databaseType) {
|
|
5
|
+
if (databaseType === "PostgreSQL") return "postgres";
|
|
6
|
+
if (databaseType === "MongoDB") return "mongodb";
|
|
7
|
+
return "mysql";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function createEnvFiles(projectDir, variables, databaseType) {
|
|
11
|
+
const env = [
|
|
12
|
+
`PORT=${variables.PORT}`,
|
|
13
|
+
`DATABASE_NAME=${variables.DATABASE_NAME}`,
|
|
14
|
+
"DB_HOST=localhost",
|
|
15
|
+
"DB_USER=root",
|
|
16
|
+
"DB_PASSWORD=",
|
|
17
|
+
`DB_DIALECT=${dbDriver(databaseType)}`,
|
|
18
|
+
`DATABASE_URL=${databaseType === "MongoDB" ? `mongodb://localhost:27017/${variables.DATABASE_NAME}` : ""}`,
|
|
19
|
+
"JWT_SECRET=change-this-secret-before-production",
|
|
20
|
+
"JWT_EXPIRES_IN=7d",
|
|
21
|
+
"CLIENT_URL=http://localhost:5173"
|
|
22
|
+
].join("\n");
|
|
23
|
+
|
|
24
|
+
const backendDir = path.join(projectDir, "backend");
|
|
25
|
+
if (await fs.pathExists(backendDir)) {
|
|
26
|
+
await fs.writeFile(path.join(backendDir, ".env"), `${env}\n`, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rootEnv = [
|
|
30
|
+
`PORT=${variables.PORT}`,
|
|
31
|
+
`DATABASE_NAME=${variables.DATABASE_NAME}`,
|
|
32
|
+
`DATABASE_TYPE=${databaseType}`
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
await fs.writeFile(path.join(projectDir, ".env.example"), `${env}\n`, "utf8");
|
|
36
|
+
await fs.writeFile(path.join(projectDir, ".env"), `${rootEnv}\n`, "utf8");
|
|
37
|
+
}
|
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
const businessCrud = ["Users", "Products", "Customers", "Orders", "Invoices", "Reports"];
|
|
5
|
+
|
|
6
|
+
function packageScripts(packageManager, stack) {
|
|
7
|
+
const scoped = {
|
|
8
|
+
npm: (workspace, script) => `npm run ${script} --workspace ${workspace}`,
|
|
9
|
+
pnpm: (workspace, script) => `pnpm --filter ${workspace} run ${script}`,
|
|
10
|
+
yarn: (workspace, script) => `yarn workspace ${workspace} ${script}`
|
|
11
|
+
}[packageManager];
|
|
12
|
+
|
|
13
|
+
if (stack === "backend") return { dev: scoped("backend", "dev"), start: scoped("backend", "start") };
|
|
14
|
+
if (stack === "frontend") return { dev: scoped("frontend", "dev"), build: scoped("frontend", "build") };
|
|
15
|
+
if (stack === "bot") return { dev: "node --watch src/index.js", start: "node src/index.js" };
|
|
16
|
+
return {
|
|
17
|
+
dev: `concurrently "${scoped("backend", "dev")}" "${scoped("frontend", "dev")}"`,
|
|
18
|
+
build: scoped("frontend", "build"),
|
|
19
|
+
start: scoped("backend", "start")
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stackFor(templateValue) {
|
|
24
|
+
if (templateValue === "express-api") return "backend";
|
|
25
|
+
if (templateValue === "react-dashboard") return "frontend";
|
|
26
|
+
if (templateValue === "bot-starter") return "bot";
|
|
27
|
+
return "fullstack";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function write(projectDir, file, content) {
|
|
31
|
+
const destination = path.join(projectDir, file);
|
|
32
|
+
await fs.ensureDir(path.dirname(destination));
|
|
33
|
+
await fs.writeFile(destination, `${content.trim()}\n`, "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rootPackage({ projectName, packageManager, stack }) {
|
|
37
|
+
const workspaces = stack === "fullstack" ? ["backend", "frontend"] : stack === "backend" ? ["backend"] : stack === "frontend" ? ["frontend"] : undefined;
|
|
38
|
+
return JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
name: projectName,
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
private: true,
|
|
43
|
+
type: "module",
|
|
44
|
+
scripts: packageScripts(packageManager, stack),
|
|
45
|
+
workspaces,
|
|
46
|
+
dependencies: stack === "fullstack" ? { concurrently: "^8.2.2" } : undefined
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2
|
|
50
|
+
).replace(/\n "workspaces": undefined,|\n "dependencies": undefined,/g, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function backendPackage() {
|
|
54
|
+
return JSON.stringify(
|
|
55
|
+
{
|
|
56
|
+
name: "backend",
|
|
57
|
+
version: "1.0.0",
|
|
58
|
+
private: true,
|
|
59
|
+
type: "module",
|
|
60
|
+
scripts: {
|
|
61
|
+
dev: "nodemon src/server.js",
|
|
62
|
+
start: "node src/server.js"
|
|
63
|
+
},
|
|
64
|
+
dependencies: {
|
|
65
|
+
bcrypt: "^5.1.1",
|
|
66
|
+
cors: "^2.8.5",
|
|
67
|
+
dotenv: "^16.4.5",
|
|
68
|
+
express: "^4.19.2",
|
|
69
|
+
"express-validator": "^7.2.0",
|
|
70
|
+
jsonwebtoken: "^9.0.2",
|
|
71
|
+
mongoose: "^8.5.2",
|
|
72
|
+
multer: "^1.4.5-lts.1",
|
|
73
|
+
mysql2: "^3.11.0",
|
|
74
|
+
pg: "^8.12.0",
|
|
75
|
+
"pg-hstore": "^2.3.4",
|
|
76
|
+
sequelize: "^6.37.3",
|
|
77
|
+
xlsx: "^0.18.5"
|
|
78
|
+
},
|
|
79
|
+
devDependencies: {
|
|
80
|
+
nodemon: "^3.1.4"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
null,
|
|
84
|
+
2
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function frontendPackage() {
|
|
89
|
+
return JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
name: "frontend",
|
|
92
|
+
version: "1.0.0",
|
|
93
|
+
private: true,
|
|
94
|
+
type: "module",
|
|
95
|
+
scripts: {
|
|
96
|
+
dev: "vite --host 0.0.0.0",
|
|
97
|
+
build: "vite build",
|
|
98
|
+
preview: "vite preview"
|
|
99
|
+
},
|
|
100
|
+
dependencies: {
|
|
101
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
102
|
+
axios: "^1.7.4",
|
|
103
|
+
"framer-motion": "^11.3.24",
|
|
104
|
+
jspdf: "^2.5.1",
|
|
105
|
+
"lucide-react": "^0.468.0",
|
|
106
|
+
react: "^19.0.0",
|
|
107
|
+
"react-dom": "^19.0.0",
|
|
108
|
+
"react-icons": "^5.2.1",
|
|
109
|
+
"react-router-dom": "^6.26.1",
|
|
110
|
+
recharts: "^2.12.7",
|
|
111
|
+
vite: "^5.4.0",
|
|
112
|
+
xlsx: "^0.18.5"
|
|
113
|
+
},
|
|
114
|
+
devDependencies: {
|
|
115
|
+
autoprefixer: "^10.4.20",
|
|
116
|
+
postcss: "^8.4.41",
|
|
117
|
+
tailwindcss: "^3.4.10"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function serverJs() {
|
|
126
|
+
return `
|
|
127
|
+
import http from "http";
|
|
128
|
+
import app from "./app.js";
|
|
129
|
+
import { connectDatabase } from "./config/database.js";
|
|
130
|
+
|
|
131
|
+
const port = Number(process.env.PORT || 5000);
|
|
132
|
+
|
|
133
|
+
async function boot() {
|
|
134
|
+
await connectDatabase();
|
|
135
|
+
http.createServer(app).listen(port, () => {
|
|
136
|
+
console.log(\`API running on http://localhost:\${port}\`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
boot().catch((error) => {
|
|
141
|
+
console.error("Failed to start server", error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function appJs() {
|
|
148
|
+
return `
|
|
149
|
+
import "dotenv/config";
|
|
150
|
+
import express from "express";
|
|
151
|
+
import cors from "cors";
|
|
152
|
+
import authRoutes from "./routes/auth.routes.js";
|
|
153
|
+
import crudRoutes from "./routes/crud.routes.js";
|
|
154
|
+
import { errorHandler, notFound } from "./middleware/error.js";
|
|
155
|
+
|
|
156
|
+
const app = express();
|
|
157
|
+
|
|
158
|
+
app.use(cors({ origin: process.env.CLIENT_URL || "http://localhost:5173", credentials: true }));
|
|
159
|
+
app.use(express.json({ limit: "10mb" }));
|
|
160
|
+
app.use(express.urlencoded({ extended: true }));
|
|
161
|
+
|
|
162
|
+
app.get("/api/health", (_req, res) => {
|
|
163
|
+
res.json({ status: "ok", service: "{{PROJECT_NAME}}", timestamp: new Date().toISOString() });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.use("/api/auth", authRoutes);
|
|
167
|
+
app.use("/api", crudRoutes);
|
|
168
|
+
app.use(notFound);
|
|
169
|
+
app.use(errorHandler);
|
|
170
|
+
|
|
171
|
+
export default app;
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function databaseJs() {
|
|
176
|
+
return `
|
|
177
|
+
import { Sequelize } from "sequelize";
|
|
178
|
+
import mongoose from "mongoose";
|
|
179
|
+
|
|
180
|
+
const dialect = process.env.DB_DIALECT || "mysql";
|
|
181
|
+
|
|
182
|
+
export const sequelize =
|
|
183
|
+
dialect === "mongodb"
|
|
184
|
+
? null
|
|
185
|
+
: new Sequelize(process.env.DATABASE_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
|
|
186
|
+
host: process.env.DB_HOST || "localhost",
|
|
187
|
+
dialect,
|
|
188
|
+
logging: false,
|
|
189
|
+
define: { underscored: true, timestamps: true }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export async function connectDatabase() {
|
|
193
|
+
if (dialect === "mongodb") {
|
|
194
|
+
await mongoose.connect(process.env.DATABASE_URL || "mongodb://localhost:27017/{{DATABASE_NAME}}");
|
|
195
|
+
console.log("MongoDB connected");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await sequelize.authenticate();
|
|
200
|
+
await sequelize.sync();
|
|
201
|
+
console.log(\`\${dialect} database connected\`);
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function authModelJs() {
|
|
207
|
+
return `
|
|
208
|
+
import bcrypt from "bcrypt";
|
|
209
|
+
import mongoose from "mongoose";
|
|
210
|
+
import { DataTypes } from "sequelize";
|
|
211
|
+
import { sequelize } from "../config/database.js";
|
|
212
|
+
|
|
213
|
+
const roles = ["Admin", "Manager", "User"];
|
|
214
|
+
|
|
215
|
+
const userFields = {
|
|
216
|
+
name: { type: String, required: true },
|
|
217
|
+
email: { type: String, required: true, unique: true, lowercase: true },
|
|
218
|
+
password: { type: String, required: true },
|
|
219
|
+
role: { type: String, enum: roles, default: "User" }
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const MongoUser = mongoose.models.User || mongoose.model("User", new mongoose.Schema(userFields, { timestamps: true }));
|
|
223
|
+
|
|
224
|
+
export const SqlUser =
|
|
225
|
+
sequelize &&
|
|
226
|
+
sequelize.define("User", {
|
|
227
|
+
name: { type: DataTypes.STRING, allowNull: false },
|
|
228
|
+
email: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { isEmail: true } },
|
|
229
|
+
password: { type: DataTypes.STRING, allowNull: false },
|
|
230
|
+
role: { type: DataTypes.ENUM(...roles), defaultValue: "User" }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export function activeUserModel() {
|
|
234
|
+
return process.env.DB_DIALECT === "mongodb" ? MongoUser : SqlUser;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function hashPassword(password) {
|
|
238
|
+
return bcrypt.hash(password, 12);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function comparePassword(password, hash) {
|
|
242
|
+
return bcrypt.compare(password, hash);
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function crudModelJs() {
|
|
248
|
+
return `
|
|
249
|
+
import mongoose from "mongoose";
|
|
250
|
+
import { DataTypes } from "sequelize";
|
|
251
|
+
import { sequelize } from "../config/database.js";
|
|
252
|
+
|
|
253
|
+
const mongoSchema = new mongoose.Schema(
|
|
254
|
+
{
|
|
255
|
+
resource: { type: String, required: true, index: true },
|
|
256
|
+
name: { type: String, required: true },
|
|
257
|
+
status: { type: String, default: "Active" },
|
|
258
|
+
amount: { type: Number, default: 0 },
|
|
259
|
+
metadata: { type: Object, default: {} }
|
|
260
|
+
},
|
|
261
|
+
{ timestamps: true }
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
export const MongoRecord = mongoose.models.Record || mongoose.model("Record", mongoSchema);
|
|
265
|
+
|
|
266
|
+
export const SqlRecord =
|
|
267
|
+
sequelize &&
|
|
268
|
+
sequelize.define("Record", {
|
|
269
|
+
resource: { type: DataTypes.STRING, allowNull: false },
|
|
270
|
+
name: { type: DataTypes.STRING, allowNull: false },
|
|
271
|
+
status: { type: DataTypes.STRING, defaultValue: "Active" },
|
|
272
|
+
amount: { type: DataTypes.FLOAT, defaultValue: 0 },
|
|
273
|
+
metadata: { type: DataTypes.JSON }
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
export function activeRecordModel() {
|
|
277
|
+
return process.env.DB_DIALECT === "mongodb" ? MongoRecord : SqlRecord;
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function authMiddlewareJs() {
|
|
283
|
+
return `
|
|
284
|
+
import jwt from "jsonwebtoken";
|
|
285
|
+
|
|
286
|
+
export function requireAuth(req, res, next) {
|
|
287
|
+
const header = req.headers.authorization || "";
|
|
288
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
|
|
289
|
+
|
|
290
|
+
if (!token) return res.status(401).json({ message: "Authentication required" });
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
294
|
+
next();
|
|
295
|
+
} catch {
|
|
296
|
+
res.status(401).json({ message: "Invalid or expired token" });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function allowRoles(...roles) {
|
|
301
|
+
return (req, res, next) => {
|
|
302
|
+
if (!roles.includes(req.user?.role)) return res.status(403).json({ message: "Insufficient permissions" });
|
|
303
|
+
next();
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function authRoutesJs() {
|
|
310
|
+
return `
|
|
311
|
+
import express from "express";
|
|
312
|
+
import jwt from "jsonwebtoken";
|
|
313
|
+
import { body, validationResult } from "express-validator";
|
|
314
|
+
import { activeUserModel, comparePassword, hashPassword } from "../models/user.model.js";
|
|
315
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
316
|
+
|
|
317
|
+
const router = express.Router();
|
|
318
|
+
|
|
319
|
+
function tokenFor(user) {
|
|
320
|
+
return jwt.sign({ id: user.id || user._id, email: user.email, role: user.role }, process.env.JWT_SECRET, {
|
|
321
|
+
expiresIn: process.env.JWT_EXPIRES_IN || "7d"
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function publicUser(user) {
|
|
326
|
+
return { id: user.id || user._id, name: user.name, email: user.email, role: user.role };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
router.post(
|
|
330
|
+
"/register",
|
|
331
|
+
body("name").notEmpty(),
|
|
332
|
+
body("email").isEmail(),
|
|
333
|
+
body("password").isLength({ min: 8 }),
|
|
334
|
+
async (req, res, next) => {
|
|
335
|
+
try {
|
|
336
|
+
const errors = validationResult(req);
|
|
337
|
+
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
338
|
+
|
|
339
|
+
const User = activeUserModel();
|
|
340
|
+
const payload = { ...req.body, password: await hashPassword(req.body.password), role: req.body.role || "User" };
|
|
341
|
+
const user = await User.create(payload);
|
|
342
|
+
res.status(201).json({ user: publicUser(user), token: tokenFor(user) });
|
|
343
|
+
} catch (error) {
|
|
344
|
+
next(error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
router.post("/login", body("email").isEmail(), body("password").notEmpty(), async (req, res, next) => {
|
|
350
|
+
try {
|
|
351
|
+
const User = activeUserModel();
|
|
352
|
+
const user =
|
|
353
|
+
process.env.DB_DIALECT === "mongodb"
|
|
354
|
+
? await User.findOne({ email: req.body.email })
|
|
355
|
+
: await User.findOne({ where: { email: req.body.email } });
|
|
356
|
+
if (!user || !(await comparePassword(req.body.password, user.password))) {
|
|
357
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
358
|
+
}
|
|
359
|
+
res.json({ user: publicUser(user), token: tokenFor(user) });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
next(error);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
router.post("/forgot-password", body("email").isEmail(), (_req, res) => {
|
|
366
|
+
res.json({ message: "Password reset instructions will be sent if the account exists." });
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
router.get("/me", requireAuth, (req, res) => {
|
|
370
|
+
res.json({ user: req.user });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
export default router;
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function crudRoutesJs() {
|
|
378
|
+
return `
|
|
379
|
+
import express from "express";
|
|
380
|
+
import multer from "multer";
|
|
381
|
+
import { Op } from "sequelize";
|
|
382
|
+
import { requireAuth, allowRoles } from "../middleware/auth.js";
|
|
383
|
+
import { activeRecordModel } from "../models/record.model.js";
|
|
384
|
+
|
|
385
|
+
const router = express.Router();
|
|
386
|
+
const upload = multer({ storage: multer.memoryStorage() });
|
|
387
|
+
const resources = ${JSON.stringify(businessCrud.map((item) => item.toLowerCase()))};
|
|
388
|
+
|
|
389
|
+
router.use(requireAuth);
|
|
390
|
+
|
|
391
|
+
function assertResource(req, res, next) {
|
|
392
|
+
if (!resources.includes(req.params.resource)) return res.status(404).json({ message: "Unknown resource" });
|
|
393
|
+
next();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
router.get("/:resource", assertResource, async (req, res, next) => {
|
|
397
|
+
try {
|
|
398
|
+
const Model = activeRecordModel();
|
|
399
|
+
const page = Number(req.query.page || 1);
|
|
400
|
+
const limit = Math.min(Number(req.query.limit || 10), 100);
|
|
401
|
+
const search = String(req.query.search || "");
|
|
402
|
+
const offset = (page - 1) * limit;
|
|
403
|
+
|
|
404
|
+
if (process.env.DB_DIALECT === "mongodb") {
|
|
405
|
+
const filter = { resource: req.params.resource, name: new RegExp(search, "i") };
|
|
406
|
+
const [items, total] = await Promise.all([
|
|
407
|
+
Model.find(filter).sort({ createdAt: -1 }).skip(offset).limit(limit),
|
|
408
|
+
Model.countDocuments(filter)
|
|
409
|
+
]);
|
|
410
|
+
return res.json({ items, total, page, pages: Math.ceil(total / limit) });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const result = await Model.findAndCountAll({
|
|
414
|
+
where: { resource: req.params.resource, name: { [Op.like]: \`%\${search}%\` } },
|
|
415
|
+
order: [["createdAt", "DESC"]],
|
|
416
|
+
limit,
|
|
417
|
+
offset
|
|
418
|
+
});
|
|
419
|
+
res.json({ items: result.rows, total: result.count, page, pages: Math.ceil(result.count / limit) });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
next(error);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
router.post("/:resource", allowRoles("Admin", "Manager"), assertResource, upload.none(), async (req, res, next) => {
|
|
426
|
+
try {
|
|
427
|
+
const Model = activeRecordModel();
|
|
428
|
+
const item = await Model.create({ ...req.body, resource: req.params.resource });
|
|
429
|
+
res.status(201).json(item);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
next(error);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
router.put("/:resource/:id", allowRoles("Admin", "Manager"), assertResource, async (req, res, next) => {
|
|
436
|
+
try {
|
|
437
|
+
const Model = activeRecordModel();
|
|
438
|
+
const item = process.env.DB_DIALECT === "mongodb" ? await Model.findByIdAndUpdate(req.params.id, req.body, { new: true }) : await Model.findByPk(req.params.id);
|
|
439
|
+
if (!item) return res.status(404).json({ message: "Record not found" });
|
|
440
|
+
if (process.env.DB_DIALECT !== "mongodb") await item.update(req.body);
|
|
441
|
+
res.json(item);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
next(error);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
router.delete("/:resource/:id", allowRoles("Admin"), assertResource, async (req, res, next) => {
|
|
448
|
+
try {
|
|
449
|
+
const Model = activeRecordModel();
|
|
450
|
+
const item = process.env.DB_DIALECT === "mongodb" ? await Model.findByIdAndDelete(req.params.id) : await Model.findByPk(req.params.id);
|
|
451
|
+
if (!item) return res.status(404).json({ message: "Record not found" });
|
|
452
|
+
if (process.env.DB_DIALECT !== "mongodb") await item.destroy();
|
|
453
|
+
res.status(204).send();
|
|
454
|
+
} catch (error) {
|
|
455
|
+
next(error);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
export default router;
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function errorJs() {
|
|
464
|
+
return `
|
|
465
|
+
export function notFound(req, res) {
|
|
466
|
+
res.status(404).json({ message: \`Route not found: \${req.originalUrl}\` });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function errorHandler(error, _req, res, _next) {
|
|
470
|
+
const status = error.status || 500;
|
|
471
|
+
res.status(status).json({
|
|
472
|
+
message: status === 500 ? "Internal server error" : error.message,
|
|
473
|
+
detail: process.env.NODE_ENV === "production" ? undefined : error.message
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function frontendFiles(title, projectName) {
|
|
480
|
+
return {
|
|
481
|
+
"frontend/package.json": frontendPackage(),
|
|
482
|
+
"frontend/index.html": `<div id="root"></div><script type="module" src="/src/main.jsx"></script>`,
|
|
483
|
+
"frontend/vite.config.js": `
|
|
484
|
+
import { defineConfig } from "vite";
|
|
485
|
+
import react from "@vitejs/plugin-react";
|
|
486
|
+
|
|
487
|
+
export default defineConfig({
|
|
488
|
+
plugins: [react()],
|
|
489
|
+
server: { port: 5173 },
|
|
490
|
+
preview: { port: 4173 }
|
|
491
|
+
});
|
|
492
|
+
`,
|
|
493
|
+
"frontend/postcss.config.js": `export default { plugins: { tailwindcss: {}, autoprefixer: {} } };`,
|
|
494
|
+
"frontend/tailwind.config.js": `
|
|
495
|
+
export default {
|
|
496
|
+
content: ["./index.html", "./src/**/*.{js,jsx}"],
|
|
497
|
+
darkMode: "class",
|
|
498
|
+
theme: { extend: { fontFamily: { sans: ["Inter", "ui-sans-serif", "system-ui"] } } },
|
|
499
|
+
plugins: []
|
|
500
|
+
};
|
|
501
|
+
`,
|
|
502
|
+
"frontend/src/main.jsx": `
|
|
503
|
+
import React from "react";
|
|
504
|
+
import { createRoot } from "react-dom/client";
|
|
505
|
+
import { BrowserRouter } from "react-router-dom";
|
|
506
|
+
import App from "./App.jsx";
|
|
507
|
+
import "./styles.css";
|
|
508
|
+
|
|
509
|
+
createRoot(document.getElementById("root")).render(
|
|
510
|
+
<React.StrictMode>
|
|
511
|
+
<BrowserRouter>
|
|
512
|
+
<App />
|
|
513
|
+
</BrowserRouter>
|
|
514
|
+
</React.StrictMode>
|
|
515
|
+
);
|
|
516
|
+
`,
|
|
517
|
+
"frontend/src/api/client.js": `
|
|
518
|
+
import axios from "axios";
|
|
519
|
+
|
|
520
|
+
export const api = axios.create({
|
|
521
|
+
baseURL: import.meta.env.VITE_API_URL || "http://localhost:5000/api"
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
api.interceptors.request.use((config) => {
|
|
525
|
+
const token = localStorage.getItem("token");
|
|
526
|
+
if (token) config.headers.Authorization = \`Bearer \${token}\`;
|
|
527
|
+
return config;
|
|
528
|
+
});
|
|
529
|
+
`,
|
|
530
|
+
"frontend/src/App.jsx": `
|
|
531
|
+
import { useMemo, useState } from "react";
|
|
532
|
+
import { Link, Navigate, Route, Routes, useNavigate } from "react-router-dom";
|
|
533
|
+
import { motion } from "framer-motion";
|
|
534
|
+
import { FiBarChart2, FiBox, FiLogOut, FiMoon, FiSearch, FiSun, FiUsers } from "react-icons/fi";
|
|
535
|
+
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
536
|
+
import { jsPDF } from "jspdf";
|
|
537
|
+
import * as XLSX from "xlsx";
|
|
538
|
+
import { api } from "./api/client.js";
|
|
539
|
+
|
|
540
|
+
const modules = ${JSON.stringify(businessCrud)};
|
|
541
|
+
const chartData = [
|
|
542
|
+
{ month: "Jan", value: 120 },
|
|
543
|
+
{ month: "Feb", value: 180 },
|
|
544
|
+
{ month: "Mar", value: 150 },
|
|
545
|
+
{ month: "Apr", value: 230 },
|
|
546
|
+
{ month: "May", value: 280 },
|
|
547
|
+
{ month: "Jun", value: 320 }
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
function AuthPage({ mode }) {
|
|
551
|
+
const navigate = useNavigate();
|
|
552
|
+
const [form, setForm] = useState({ name: "", email: "admin@diolo.app", password: "password123", role: "Admin" });
|
|
553
|
+
const [error, setError] = useState("");
|
|
554
|
+
|
|
555
|
+
async function submit(event) {
|
|
556
|
+
event.preventDefault();
|
|
557
|
+
try {
|
|
558
|
+
if (mode === "forgot") {
|
|
559
|
+
await api.post("/auth/forgot-password", { email: form.email });
|
|
560
|
+
navigate("/login");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const endpoint = mode === "register" ? "/auth/register" : "/auth/login";
|
|
564
|
+
const { data } = await api.post(endpoint, form);
|
|
565
|
+
localStorage.setItem("token", data.token);
|
|
566
|
+
localStorage.setItem("user", JSON.stringify(data.user));
|
|
567
|
+
navigate("/");
|
|
568
|
+
} catch (err) {
|
|
569
|
+
setError(err.response?.data?.message || "Please check your details and try again.");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<main className="min-h-screen bg-slate-950 px-4 py-10 text-white">
|
|
575
|
+
<section className="mx-auto grid min-h-[calc(100vh-5rem)] max-w-6xl items-center gap-8 md:grid-cols-[1.1fr_.9fr]">
|
|
576
|
+
<div>
|
|
577
|
+
<p className="text-sm font-semibold uppercase tracking-wide text-cyan-300">${title}</p>
|
|
578
|
+
<h1 className="mt-4 text-4xl font-bold md:text-6xl">{mode === "forgot" ? "Recover access" : "Run business operations with confidence."}</h1>
|
|
579
|
+
<p className="mt-5 max-w-2xl text-lg text-slate-300">Role-based dashboards, CRUD workflows, charts, exports, search, pagination, and responsive layouts are wired from day one.</p>
|
|
580
|
+
</div>
|
|
581
|
+
<form onSubmit={submit} className="rounded-lg border border-slate-800 bg-white p-6 text-slate-950 shadow-2xl">
|
|
582
|
+
<h2 className="text-2xl font-bold">{mode === "register" ? "Create account" : mode === "forgot" ? "Forgot password" : "Login"}</h2>
|
|
583
|
+
{mode === "register" && <input className="mt-5 input" placeholder="Full name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />}
|
|
584
|
+
<input className="mt-4 input" placeholder="Email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
|
|
585
|
+
{mode !== "forgot" && <input className="mt-4 input" type="password" placeholder="Password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} />}
|
|
586
|
+
{mode === "register" && (
|
|
587
|
+
<select className="mt-4 input" value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })}>
|
|
588
|
+
<option>Admin</option><option>Manager</option><option>User</option>
|
|
589
|
+
</select>
|
|
590
|
+
)}
|
|
591
|
+
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
|
592
|
+
<button className="mt-5 w-full rounded-md bg-slate-950 px-4 py-3 font-semibold text-white">{mode === "forgot" ? "Send reset link" : "Continue"}</button>
|
|
593
|
+
<div className="mt-4 flex justify-between text-sm text-slate-600">
|
|
594
|
+
<Link to="/login">Login</Link>
|
|
595
|
+
<Link to="/register">Register</Link>
|
|
596
|
+
<Link to="/forgot-password">Forgot?</Link>
|
|
597
|
+
</div>
|
|
598
|
+
</form>
|
|
599
|
+
</section>
|
|
600
|
+
</main>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function Dashboard() {
|
|
605
|
+
const [dark, setDark] = useState(false);
|
|
606
|
+
const [active, setActive] = useState("Users");
|
|
607
|
+
const [query, setQuery] = useState("");
|
|
608
|
+
const [page, setPage] = useState(1);
|
|
609
|
+
const user = JSON.parse(localStorage.getItem("user") || '{"name":"Admin","role":"Admin"}');
|
|
610
|
+
const rows = useMemo(() => Array.from({ length: 18 }, (_, index) => ({
|
|
611
|
+
id: index + 1,
|
|
612
|
+
name: \`\${active.slice(0, -1)} \${index + 1}\`,
|
|
613
|
+
status: index % 3 === 0 ? "Pending" : "Active",
|
|
614
|
+
amount: 1200 + index * 135
|
|
615
|
+
})).filter((row) => row.name.toLowerCase().includes(query.toLowerCase())), [active, query]);
|
|
616
|
+
const visibleRows = rows.slice((page - 1) * 6, page * 6);
|
|
617
|
+
|
|
618
|
+
function logout() {
|
|
619
|
+
localStorage.clear();
|
|
620
|
+
window.location.href = "/login";
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function exportPdf() {
|
|
624
|
+
const doc = new jsPDF();
|
|
625
|
+
doc.text(\`\${active} Report\`, 14, 18);
|
|
626
|
+
visibleRows.forEach((row, index) => doc.text(\`\${row.id}. \${row.name} - \${row.status} - $\${row.amount}\`, 14, 30 + index * 8));
|
|
627
|
+
doc.save(\`\${active.toLowerCase()}-report.pdf\`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function exportExcel() {
|
|
631
|
+
const sheet = XLSX.utils.json_to_sheet(visibleRows);
|
|
632
|
+
const book = XLSX.utils.book_new();
|
|
633
|
+
XLSX.utils.book_append_sheet(book, sheet, active);
|
|
634
|
+
XLSX.writeFile(book, \`\${active.toLowerCase()}-report.xlsx\`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return (
|
|
638
|
+
<div className={dark ? "dark" : ""}>
|
|
639
|
+
<div className="min-h-screen bg-slate-100 text-slate-950 dark:bg-slate-950 dark:text-white">
|
|
640
|
+
<aside className="fixed inset-y-0 left-0 hidden w-64 border-r border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900 lg:block">
|
|
641
|
+
<div className="flex items-center gap-3 text-xl font-bold"><FiBox /> ${projectName}</div>
|
|
642
|
+
<nav className="mt-8 space-y-1">
|
|
643
|
+
{modules.map((module) => (
|
|
644
|
+
<button key={module} onClick={() => { setActive(module); setPage(1); }} className={\`nav-item \${active === module ? "nav-active" : ""}\`}>
|
|
645
|
+
{module === "Users" ? <FiUsers /> : <FiBarChart2 />} {module}
|
|
646
|
+
</button>
|
|
647
|
+
))}
|
|
648
|
+
</nav>
|
|
649
|
+
</aside>
|
|
650
|
+
<main className="lg:pl-64">
|
|
651
|
+
<header className="sticky top-0 z-10 flex items-center justify-between border-b border-slate-200 bg-white/90 px-5 py-4 backdrop-blur dark:border-slate-800 dark:bg-slate-900/90">
|
|
652
|
+
<div>
|
|
653
|
+
<p className="text-sm text-slate-500 dark:text-slate-400">{user.role}</p>
|
|
654
|
+
<h1 className="text-xl font-bold">${title}</h1>
|
|
655
|
+
</div>
|
|
656
|
+
<div className="flex items-center gap-2">
|
|
657
|
+
<button className="icon-btn" onClick={() => setDark(!dark)} aria-label="Toggle dark mode">{dark ? <FiSun /> : <FiMoon />}</button>
|
|
658
|
+
<button className="icon-btn" onClick={logout} aria-label="Logout"><FiLogOut /></button>
|
|
659
|
+
</div>
|
|
660
|
+
</header>
|
|
661
|
+
<section className="grid gap-5 p-5 md:grid-cols-3">
|
|
662
|
+
{["Revenue", "Orders", "Customers"].map((label, index) => (
|
|
663
|
+
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }} className="metric" key={label}>
|
|
664
|
+
<p>{label}</p><strong>{index === 0 ? "$84,220" : index === 1 ? "1,284" : "642"}</strong>
|
|
665
|
+
</motion.div>
|
|
666
|
+
))}
|
|
667
|
+
</section>
|
|
668
|
+
<section className="grid gap-5 px-5 pb-5 xl:grid-cols-[1fr_1.3fr]">
|
|
669
|
+
<div className="panel h-80">
|
|
670
|
+
<h2 className="section-title">Performance</h2>
|
|
671
|
+
<ResponsiveContainer width="100%" height="85%">
|
|
672
|
+
<AreaChart data={chartData}><CartesianGrid strokeDasharray="3 3" /><XAxis dataKey="month" /><YAxis /><Tooltip /><Area type="monotone" dataKey="value" stroke="#0891b2" fill="#67e8f9" /></AreaChart>
|
|
673
|
+
</ResponsiveContainer>
|
|
674
|
+
</div>
|
|
675
|
+
<div className="panel">
|
|
676
|
+
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
677
|
+
<h2 className="section-title">{active}</h2>
|
|
678
|
+
<div className="flex flex-wrap gap-2">
|
|
679
|
+
<label className="search"><FiSearch /><input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search" /></label>
|
|
680
|
+
<button className="action-btn" onClick={exportPdf}>Export PDF</button>
|
|
681
|
+
<button className="action-btn" onClick={exportExcel}>Export Excel</button>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
<div className="mt-5 overflow-x-auto">
|
|
685
|
+
<table className="w-full min-w-[620px] text-left text-sm">
|
|
686
|
+
<thead><tr className="border-b border-slate-200 dark:border-slate-800"><th>ID</th><th>Name</th><th>Status</th><th>Amount</th></tr></thead>
|
|
687
|
+
<tbody>{visibleRows.map((row) => <tr key={row.id} className="border-b border-slate-100 dark:border-slate-800"><td>{row.id}</td><td>{row.name}</td><td><span className="badge">{row.status}</span></td><td>{\`$\${row.amount.toLocaleString()}\`}</td></tr>)}</tbody>
|
|
688
|
+
</table>
|
|
689
|
+
</div>
|
|
690
|
+
<div className="mt-4 flex items-center justify-between">
|
|
691
|
+
<button className="action-btn" disabled={page === 1} onClick={() => setPage(page - 1)}>Previous</button>
|
|
692
|
+
<span className="text-sm text-slate-500">Page {page}</span>
|
|
693
|
+
<button className="action-btn" disabled={page * 6 >= rows.length} onClick={() => setPage(page + 1)}>Next</button>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</section>
|
|
697
|
+
</main>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function Protected() {
|
|
704
|
+
return localStorage.getItem("token") ? <Dashboard /> : <Navigate to="/login" replace />;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export default function App() {
|
|
708
|
+
return (
|
|
709
|
+
<Routes>
|
|
710
|
+
<Route path="/" element={<Protected />} />
|
|
711
|
+
<Route path="/login" element={<AuthPage mode="login" />} />
|
|
712
|
+
<Route path="/register" element={<AuthPage mode="register" />} />
|
|
713
|
+
<Route path="/forgot-password" element={<AuthPage mode="forgot" />} />
|
|
714
|
+
</Routes>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
`,
|
|
718
|
+
"frontend/src/styles.css": `
|
|
719
|
+
@tailwind base;
|
|
720
|
+
@tailwind components;
|
|
721
|
+
@tailwind utilities;
|
|
722
|
+
|
|
723
|
+
* { box-sizing: border-box; }
|
|
724
|
+
body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
|
|
725
|
+
th, td { padding: 0.85rem 0.7rem; }
|
|
726
|
+
.input { width: 100%; border: 1px solid #cbd5e1; border-radius: 0.5rem; padding: 0.8rem 0.9rem; outline: none; }
|
|
727
|
+
.input:focus { border-color: #0891b2; box-shadow: 0 0 0 3px rgb(8 145 178 / 15%); }
|
|
728
|
+
.nav-item { display: flex; width: 100%; align-items: center; gap: 0.75rem; border-radius: 0.5rem; padding: 0.75rem; color: #475569; }
|
|
729
|
+
.nav-active, .nav-item:hover { background: #ecfeff; color: #0e7490; }
|
|
730
|
+
.dark .nav-active, .dark .nav-item:hover { background: #164e63; color: white; }
|
|
731
|
+
.icon-btn { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; }
|
|
732
|
+
.metric, .panel { border: 1px solid #e2e8f0; border-radius: 0.5rem; background: white; padding: 1.25rem; box-shadow: 0 8px 30px rgb(15 23 42 / 5%); }
|
|
733
|
+
.dark .metric, .dark .panel { border-color: #1e293b; background: #0f172a; }
|
|
734
|
+
.metric p { color: #64748b; font-size: 0.9rem; }
|
|
735
|
+
.metric strong { display: block; margin-top: 0.5rem; font-size: 1.8rem; }
|
|
736
|
+
.section-title { font-size: 1.1rem; font-weight: 800; }
|
|
737
|
+
.search { display: flex; align-items: center; gap: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; padding: 0.6rem 0.75rem; }
|
|
738
|
+
.search input { min-width: 10rem; border: 0; background: transparent; outline: none; }
|
|
739
|
+
.action-btn { border: 1px solid #cbd5e1; border-radius: 0.5rem; padding: 0.6rem 0.85rem; font-weight: 700; }
|
|
740
|
+
.action-btn:disabled { opacity: 0.45; }
|
|
741
|
+
.badge { border-radius: 999px; background: #dcfce7; color: #166534; padding: 0.25rem 0.55rem; font-size: 0.75rem; font-weight: 700; }
|
|
742
|
+
`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function readme({ title, databaseType, stack }) {
|
|
747
|
+
return `
|
|
748
|
+
# {{PROJECT_NAME}}
|
|
749
|
+
|
|
750
|
+
Generated with create-diolo-app.
|
|
751
|
+
|
|
752
|
+
## Stack
|
|
753
|
+
|
|
754
|
+
- ${stack === "frontend" ? "React 19, Vite, Tailwind CSS" : stack === "backend" ? "Node.js, Express, JWT, Sequelize, MongoDB support" : "React 19, Vite, Tailwind CSS, Node.js, Express, JWT"}
|
|
755
|
+
- Database: ${databaseType}
|
|
756
|
+
- Role-based access: Admin, Manager, User
|
|
757
|
+
- Dashboard: sidebar, navbar, dark mode, charts, tables, pagination, search, PDF and Excel exports
|
|
758
|
+
- CRUD modules: ${businessCrud.join(", ")}
|
|
759
|
+
|
|
760
|
+
## Getting Started
|
|
761
|
+
|
|
762
|
+
\`\`\`bash
|
|
763
|
+
npm install
|
|
764
|
+
npm run dev
|
|
765
|
+
\`\`\`
|
|
766
|
+
|
|
767
|
+
Backend runs on http://localhost:{{PORT}} and frontend runs on http://localhost:5173.
|
|
768
|
+
|
|
769
|
+
## Environment
|
|
770
|
+
|
|
771
|
+
Update \`backend/.env\` before production. Set a strong \`JWT_SECRET\`, database credentials, and allowed \`CLIENT_URL\`.
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function botFiles({ projectName }) {
|
|
776
|
+
return {
|
|
777
|
+
"package.json": JSON.stringify(
|
|
778
|
+
{
|
|
779
|
+
name: projectName,
|
|
780
|
+
version: "1.0.0",
|
|
781
|
+
private: true,
|
|
782
|
+
type: "module",
|
|
783
|
+
scripts: { dev: "node --watch src/index.js", start: "node src/index.js" },
|
|
784
|
+
dependencies: { dotenv: "^16.4.5" }
|
|
785
|
+
},
|
|
786
|
+
null,
|
|
787
|
+
2
|
|
788
|
+
),
|
|
789
|
+
"src/index.js": `
|
|
790
|
+
import "dotenv/config";
|
|
791
|
+
|
|
792
|
+
const botName = process.env.BOT_NAME || "{{PROJECT_NAME}}";
|
|
793
|
+
|
|
794
|
+
async function startBot() {
|
|
795
|
+
console.log(\`\${botName} is online.\`);
|
|
796
|
+
console.log("Add your queue, webhook, chat, or automation adapter in src/index.js.");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
startBot().catch((error) => {
|
|
800
|
+
console.error("Bot failed", error);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
});
|
|
803
|
+
`,
|
|
804
|
+
".env": "BOT_NAME={{PROJECT_NAME}}",
|
|
805
|
+
"README.md": "# {{PROJECT_NAME}}\n\nNode.js bot starter generated with create-diolo-app.\n\n```bash\nnpm install\nnpm run dev\n```"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export async function generateProjectFiles(projectDir, options) {
|
|
810
|
+
const stack = stackFor(options.template);
|
|
811
|
+
const title = options.templateConfig.name;
|
|
812
|
+
|
|
813
|
+
if (stack === "bot") {
|
|
814
|
+
for (const [file, content] of Object.entries(botFiles(options))) await write(projectDir, file, content);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
await write(projectDir, "package.json", rootPackage({ ...options, stack }));
|
|
819
|
+
await write(projectDir, ".gitignore", "node_modules\n.env\n.DS_Store\ndist\ncoverage\nuploads\n");
|
|
820
|
+
await write(projectDir, "README.md", readme({ title, databaseType: options.databaseType, stack }));
|
|
821
|
+
|
|
822
|
+
if (stack === "fullstack" || stack === "backend") {
|
|
823
|
+
const backend = {
|
|
824
|
+
"backend/package.json": backendPackage(),
|
|
825
|
+
"backend/src/server.js": serverJs(),
|
|
826
|
+
"backend/src/app.js": appJs(),
|
|
827
|
+
"backend/src/config/database.js": databaseJs(),
|
|
828
|
+
"backend/src/models/user.model.js": authModelJs(),
|
|
829
|
+
"backend/src/models/record.model.js": crudModelJs(),
|
|
830
|
+
"backend/src/middleware/auth.js": authMiddlewareJs(),
|
|
831
|
+
"backend/src/middleware/error.js": errorJs(),
|
|
832
|
+
"backend/src/routes/auth.routes.js": authRoutesJs(),
|
|
833
|
+
"backend/src/routes/crud.routes.js": crudRoutesJs()
|
|
834
|
+
};
|
|
835
|
+
for (const [file, content] of Object.entries(backend)) await write(projectDir, file, content);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (stack === "fullstack" || stack === "frontend") {
|
|
839
|
+
for (const [file, content] of Object.entries(frontendFiles(title, options.projectName))) await write(projectDir, file, content);
|
|
840
|
+
}
|
|
841
|
+
}
|
package/utils/gitInit.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
|
|
4
|
+
export async function gitInit(projectDir) {
|
|
5
|
+
const spinner = ora("Initializing Git repository...").start();
|
|
6
|
+
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const child = spawn("git", ["init"], {
|
|
9
|
+
cwd: projectDir,
|
|
10
|
+
stdio: "pipe",
|
|
11
|
+
shell: process.platform === "win32"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
child.on("close", (code) => {
|
|
15
|
+
if (code === 0) {
|
|
16
|
+
spinner.succeed("Git repository initialized.");
|
|
17
|
+
} else {
|
|
18
|
+
spinner.warn("Git was not initialized. You can run git init manually.");
|
|
19
|
+
}
|
|
20
|
+
resolve();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
|
|
4
|
+
const installCommands = {
|
|
5
|
+
npm: ["install"],
|
|
6
|
+
pnpm: ["install"],
|
|
7
|
+
yarn: ["install"]
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function installDependencies(projectDir, packageManager) {
|
|
11
|
+
const spinner = ora(`Installing dependencies with ${packageManager}...`).start();
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const child = spawn(packageManager, installCommands[packageManager], {
|
|
15
|
+
cwd: projectDir,
|
|
16
|
+
stdio: "pipe",
|
|
17
|
+
shell: process.platform === "win32"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let output = "";
|
|
21
|
+
child.stdout.on("data", (data) => {
|
|
22
|
+
output += data.toString();
|
|
23
|
+
});
|
|
24
|
+
child.stderr.on("data", (data) => {
|
|
25
|
+
output += data.toString();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.on("close", (code) => {
|
|
29
|
+
if (code === 0) {
|
|
30
|
+
spinner.succeed("Dependencies installed.");
|
|
31
|
+
resolve();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spinner.fail("Dependency installation failed.");
|
|
36
|
+
reject(new Error(output || `${packageManager} install exited with code ${code}`));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
const textExtensions = new Set([
|
|
5
|
+
".js",
|
|
6
|
+
".jsx",
|
|
7
|
+
".ts",
|
|
8
|
+
".tsx",
|
|
9
|
+
".json",
|
|
10
|
+
".md",
|
|
11
|
+
".env",
|
|
12
|
+
".txt",
|
|
13
|
+
".html",
|
|
14
|
+
".css",
|
|
15
|
+
".sql"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export async function replaceVariables(targetDir, variables) {
|
|
19
|
+
const entries = await fs.readdir(targetDir);
|
|
20
|
+
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const entryPath = path.join(targetDir, entry);
|
|
23
|
+
const stat = await fs.stat(entryPath);
|
|
24
|
+
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
await replaceVariables(entryPath, variables);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!textExtensions.has(path.extname(entryPath))) continue;
|
|
31
|
+
|
|
32
|
+
let content = await fs.readFile(entryPath, "utf8");
|
|
33
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
34
|
+
content = content.replaceAll(`{{${key}}}`, String(value));
|
|
35
|
+
}
|
|
36
|
+
await fs.writeFile(entryPath, content, "utf8");
|
|
37
|
+
}
|
|
38
|
+
}
|