tss-stack 1.1.2 → 1.2.1

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/README.md CHANGED
@@ -115,13 +115,32 @@ SmartPark/
115
115
  - **`routes/auth.js`** — Register, login, logout endpoints (if auth selected)
116
116
  - **`.env.example`** — Template for your database credentials
117
117
 
118
- ### Frontend (`frontend-project/`)
119
-
120
- - **`src/api/axios.js`** — Pre-configured Axios instance pointed at your backend, `withCredentials` enabled
121
- - **`src/pages/*.jsx`** — One page per table with form, table display, and only the action buttons matching your selected operations
122
- - **`src/pages/Login.jsx`** — Login form wired to `/auth/login` (if auth selected)
118
+ ## Frontend (`frontend-project/`)
119
+
120
+ - **`vite.config.js`** — Vite configuration with React plugin
121
+ - **`tailwind.config.js`** — Tailwind CSS configuration
122
+ - **`postcss.config.js`** — PostCSS plugins (Tailwind + Autoprefixer)
123
+ - **`index.html`** — Entry HTML file (required by Vite)
124
+ - **`.env.local.example`** — Environment template for API URL
125
+ - **`.gitignore`** — Git ignore rules for frontend
126
+ - **`src/api/axios.js`** — Pre-configured Axios instance with environment-based URL
127
+ - **`src/pages/*.jsx`** — One page per table with:
128
+ - Error handling and error messages
129
+ - Loading states on form submission
130
+ - Success notifications
131
+ - Automatic data fetching on page load
132
+ - Complete CRUD operations (only those selected)
133
+ - **`src/pages/Home.jsx`** — Landing page
134
+ - **`src/pages/Login.jsx`** — Login/Register page with toggle (if auth selected)
123
135
  - **`src/pages/Reports.jsx`** — Reports page scaffold (if selected)
124
- - **`src/App.jsx`** — React Router setup with Navbar and all routes configured
136
+ - **`src/components/PrivateRoute.jsx`** — Route protection component (if auth selected)
137
+ - **`src/context/AuthContext.jsx`** — Auth provider with session checking (if auth selected)
138
+ - **`src/App.jsx`** — React Router setup with:
139
+ - Protected routes (if auth selected)
140
+ - Navbar with conditional logout button
141
+ - Auth context provider wrapping app
142
+ - **`src/main.jsx`** — React entry point
143
+ - **`src/index.css`** — Tailwind CSS imports
125
144
 
126
145
  ---
127
146
 
@@ -133,7 +152,7 @@ SmartPark/
133
152
  mysql -u root -p < my-app/backend-project/config/database.sql
134
153
  ```
135
154
 
136
- ### 2. Configure environment
155
+ ### 2. Configure backend environment
137
156
 
138
157
  ```bash
139
158
  cd my-app/backend-project
@@ -148,31 +167,101 @@ DB_USER=root
148
167
  DB_PASSWORD=your_password
149
168
  DB_NAME=smartpark_db
150
169
  PORT=5000
151
- SESSION_SECRET=any_random_string_here
170
+ SESSION_SECRET=your_random_secret_string_here
171
+ CLIENT_URL=http://localhost:5173
172
+ NODE_ENV=development
152
173
  ```
153
174
 
154
- ### 3. Start the backend
175
+ ### 3. Configure frontend environment (optional)
155
176
 
156
177
  ```bash
178
+ cd ../frontend-project
179
+ cp .env.local.example .env.local
180
+ ```
181
+
182
+ By default, the frontend connects to `http://localhost:5000`. Override in `.env.local` if needed:
183
+
184
+ ```env
185
+ VITE_API_URL=http://your-api-url:5000
186
+ ```
187
+
188
+ ### 4. Start the backend
189
+
190
+ ```bash
191
+ cd my-app/backend-project
192
+ npm install
157
193
  npm run dev
158
194
  ```
159
195
 
160
- ### 4. Start the frontend
196
+ Server runs on `http://localhost:5000`
197
+
198
+ ### 5. Start the frontend
161
199
 
162
200
  ```bash
163
201
  cd ../frontend-project
202
+ npm install
164
203
  npm run dev
165
204
  ```
166
205
 
167
- ### 5. Open the app
206
+ Frontend runs on `http://localhost:5173`
168
207
 
169
- ```
170
- http://localhost:5173
171
- ```
208
+ ### 6. Open in browser
209
+
210
+ Visit `http://localhost:5173`
211
+
212
+ If auth is enabled, register or login first. Then access your data tables.
172
213
 
173
214
  ---
174
215
 
175
- ## Requirements
216
+ ## Features
217
+
218
+ ✅ **Authentication** (optional)
219
+ - Login/Register with bcrypt hashing
220
+ - Session management
221
+ - Route protection
222
+ - Auto session check on load
223
+ - Logout functionality
224
+
225
+ ✅ **Full CRUD Interface**
226
+ - Form validation
227
+ - Error handling with messages
228
+ - Success notifications
229
+ - Loading states
230
+ - Auto data refresh
231
+
232
+ ✅ **Frontend Stack**
233
+ - React 18 + Vite
234
+ - React Router v6
235
+ - Tailwind CSS
236
+ - Axios with credentials
237
+
238
+ ✅ **Backend Stack**
239
+ - Express.js
240
+ - MySQL connection pooling
241
+ - CORS & security headers
242
+ - Rate limiting
243
+ - Session management
244
+
245
+ ✅ **Database**
246
+ - Auto table creation
247
+ - Timestamps (created_at, updated_at)
248
+ - Proper SQL types
249
+ - Auto-increment IDs
250
+
251
+ ---
252
+
253
+ ## Stack Summary
254
+
255
+ | Layer | Technology |
256
+ |---|---|
257
+ | Runtime | Node.js |
258
+ | Backend | Express.js |
259
+ | Database | MySQL (mysql2) |
260
+ | Auth | express-session + bcryptjs |
261
+ | Frontend | React 18 + Vite |
262
+ | CSS | Tailwind CSS |
263
+ | HTTP | Axios |
264
+ | Routing | React Router v6 |
176
265
 
177
266
  - Node.js 16 or higher
178
267
  - npm 7 or higher
@@ -194,44 +283,6 @@ tss-stack/
194
283
  │ └── utils.js ← shared helpers (toPascal, toRoute, toCamel)
195
284
  └── package.json
196
285
  ```
197
-
198
- ---
199
-
200
- ## Local Development
201
-
202
- Clone the repo and link it globally to test before publishing:
203
-
204
- ```bash
205
- git clone https://github.com/your-username/tss-stack.git
206
- cd tss-stack
207
- npm install
208
- npm link
209
- ```
210
-
211
- Now test it like a real user:
212
-
213
- ```bash
214
- tss-stack test-project
215
- ```
216
-
217
- When done testing:
218
-
219
- ```bash
220
- npm unlink -g tss-stack
221
- ```
222
-
223
- ---
224
-
225
- ## Publishing a New Version
226
-
227
- ```bash
228
- npm version patch # bug fix: 1.0.0 → 1.0.1
229
- npm version minor # new feature: 1.0.0 → 1.1.0
230
- npm version major # breaking: 1.0.0 → 2.0.0
231
-
232
- npm publish
233
- ```
234
-
235
286
  ---
236
287
 
237
288
  ## Roadmap
package/bin/cli.js CHANGED
@@ -310,14 +310,26 @@ process.stdout.write("\x1Bc");
310
310
 
311
311
  async function showProjectTree(config) {
312
312
  const backendRouteFiles = [
313
- config.needsAuth ? "│ ├── auth.js" : null,
314
- ...config.tables.map((t) => `│ ├── ${t.name}.js`),
313
+ config.needsAuth ? "│ ├── auth.js" : null,
314
+ ...config.tables.map((t, i) => {
315
+ const isLast = i === config.tables.length - 1;
316
+ return `│ │ ${isLast && !config.tables.slice(i + 1).length ? "└──" : "├──"} ${t.name}.js`;
317
+ }),
315
318
  ].filter(Boolean);
316
319
 
320
+ const frontendContextFiles = config.needsAuth ? [
321
+ "│ │ ├── AuthContext.jsx",
322
+ ] : [];
323
+
324
+ const frontendComponentFiles = config.needsAuth ? [
325
+ "│ │ └── PrivateRoute.jsx",
326
+ ] : [];
327
+
317
328
  const frontendPageFiles = [
318
- config.needsAuth ? "│ ├── Login.jsx" : null,
319
- ...config.tables.map((t) => `│ ├── ${toPascal(t.name)}.jsx`),
320
- config.needsReports ? "└── Reports.jsx" : null,
329
+ "│ ├── Home.jsx",
330
+ config.needsAuth ? "│ ├── Login.jsx" : null,
331
+ ...config.tables.map((t) => `│ ├── ${toPascal(t.name)}.jsx`),
332
+ config.needsReports ? "│ │ └── Reports.jsx" : null,
321
333
  ].filter(Boolean);
322
334
 
323
335
  const tree = [
@@ -328,9 +340,9 @@ async function showProjectTree(config) {
328
340
  "│ │ ├── db.js",
329
341
  "│ │ └── database.sql",
330
342
  "│ ├── middleware/",
331
- config.needsAuth ? "│ │ └── auth.js" : null,
343
+ config.needsAuth ? "│ │ └── auth.js" : "│ │ (none)",
332
344
  "│ ├── routes/",
333
- ...formatTree(backendRouteFiles).map((line) => `│ ${line}`),
345
+ ...backendRouteFiles,
334
346
  "│ ├── server.js",
335
347
  "│ ├── .env.example",
336
348
  "│ └── package.json",
@@ -339,10 +351,21 @@ async function showProjectTree(config) {
339
351
  " ├── src/",
340
352
  " │ ├── api/",
341
353
  " │ │ └── axios.js",
354
+ config.needsAuth ? " │ ├── context/" : null,
355
+ ...frontendContextFiles,
356
+ config.needsAuth ? " │ ├── components/" : null,
357
+ ...frontendComponentFiles,
342
358
  " │ ├── pages/",
343
- ...formatTree(frontendPageFiles).map((line) => ` │ ${line}`),
359
+ ...frontendPageFiles,
344
360
  " │ ├── App.jsx",
345
- " │ └── main.jsx",
361
+ " │ ├── main.jsx",
362
+ " │ └── index.css",
363
+ " ├── vite.config.js",
364
+ " ├── tailwind.config.js",
365
+ " ├── postcss.config.js",
366
+ " ├── index.html",
367
+ " ├── .env.local.example",
368
+ " ├── .gitignore",
346
369
  " └── package.json",
347
370
  ].filter(Boolean);
348
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tss-stack",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Interactive full-stack Node.js + React + MySQL project generator",
5
5
  "bin": {
6
6
  "tss-stack": "bin/cli.js"
@@ -53,12 +53,25 @@ DB_USER=root
53
53
  DB_PASSWORD=your_password_here
54
54
  DB_NAME=${dbName}
55
55
  PORT=${port}
56
- SESSION_SECRET=change_me
56
+ SESSION_SECRET=change_me_to_random_string
57
57
  CLIENT_URL=http://localhost:5173
58
58
  NODE_ENV=development
59
59
  `
60
60
  );
61
61
 
62
+ await fs.outputFile(
63
+ path.join(root, ".gitignore"),
64
+ `node_modules/
65
+ .env
66
+ .env.local
67
+ *.log
68
+ npm-debug.log*
69
+ .DS_Store
70
+ .idea/
71
+ .vscode/
72
+ `
73
+ );
74
+
62
75
  await fs.outputFile(
63
76
  path.join(root, "config", "db.js"),
64
77
  `const mysql = require("mysql2");
@@ -104,15 +117,14 @@ const bcrypt = require("bcryptjs");
104
117
  const rateLimit = require("express-rate-limit");
105
118
  const router = express.Router();
106
119
  const db = require("../config/db");
120
+ const isAuthenticated = require("../middleware/auth");
107
121
 
108
122
  const authLimiter = rateLimit({
109
123
  windowMs: 15 * 60 * 1000,
110
124
  max: 50,
111
125
  });
112
126
 
113
- router.use(authLimiter);
114
-
115
- router.post("/register", async (req, res) => {
127
+ router.post("/register", authLimiter, async (req, res) => {
116
128
  try {
117
129
  const { username, password } = req.body;
118
130
 
@@ -126,29 +138,40 @@ router.post("/register", async (req, res) => {
126
138
 
127
139
  const hash = await bcrypt.hash(password, 10);
128
140
 
129
- await db.query("INSERT INTO users (username, password) VALUES (?, ?)", [username, hash]);
130
-
131
- res.json({ message: "User registered successfully" });
141
+ try {
142
+ await db.query("INSERT INTO users (username, password) VALUES (?, ?)", [username, hash]);
143
+ res.json({ message: "User registered successfully" });
144
+ } catch (err) {
145
+ if (err.code === "ER_DUP_ENTRY") {
146
+ return res.status(400).json({ message: "Username already exists" });
147
+ }
148
+ throw err;
149
+ }
132
150
  } catch (err) {
133
151
  res.status(500).json({ error: err.message });
134
152
  }
135
153
  });
136
154
 
137
- router.post("/login", async (req, res) => {
155
+ router.post("/login", authLimiter, async (req, res) => {
138
156
  try {
139
157
  const { username, password } = req.body;
140
158
 
141
- const [results] = await db.query("SELECT * FROM users WHERE username = ?", [username]);
159
+ if (!username || !password) {
160
+ return res.status(400).json({ message: "Username and password required" });
161
+ }
162
+
163
+ const [results] = await db.query("SELECT id, username FROM users WHERE username = ? LIMIT 1", [username]);
142
164
 
143
165
  if (results.length === 0) {
144
- return res.status(401).json({ message: "User not found" });
166
+ return res.status(401).json({ message: "Invalid credentials" });
145
167
  }
146
168
 
147
169
  const user = results[0];
148
- const passwordMatch = await bcrypt.compare(password, user.password);
170
+ const [userWithPassword] = await db.query("SELECT password FROM users WHERE id = ?", [user.id]);
171
+ const passwordMatch = await bcrypt.compare(password, userWithPassword[0].password);
149
172
 
150
173
  if (!passwordMatch) {
151
- return res.status(401).json({ message: "Wrong password" });
174
+ return res.status(401).json({ message: "Invalid credentials" });
152
175
  }
153
176
 
154
177
  req.session.user = {
@@ -165,8 +188,13 @@ router.post("/login", async (req, res) => {
165
188
  }
166
189
  });
167
190
 
191
+ router.get("/me", isAuthenticated, (req, res) => {
192
+ res.json(req.session.user);
193
+ });
194
+
168
195
  router.post("/logout", (req, res) => {
169
- req.session.destroy(() => {
196
+ req.session.destroy((err) => {
197
+ if (err) return res.status(500).json({ error: "Logout failed" });
170
198
  res.json({ message: "Logged out" });
171
199
  });
172
200
  });
@@ -38,7 +38,8 @@ CREATE TABLE IF NOT EXISTS ${escapeSqlIdentifier(table.name)} (
38
38
  `;
39
39
  }
40
40
 
41
- sql += ` created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
41
+ sql += ` created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
42
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
42
43
  );
43
44
 
44
45
  `;
@@ -3,51 +3,128 @@ const path = require("path");
3
3
  const { toPascal, toRoute } = require("./utils");
4
4
 
5
5
  function generateFrontend(config) {
6
- const { projectName, tables, needsAuth, needsReports, targetDir } = config;
7
- const root = path.join(targetDir, "frontend-project");
6
+ const { projectName, tables, needsAuth, needsReports, targetDir } = config;
7
+ const root = path.join(targetDir, "frontend-project");
8
+
9
+ fs.outputFileSync(
10
+ path.join(root, "package.json"),
11
+ JSON.stringify(
12
+ {
13
+ name: "frontend-project",
14
+ version: "1.0.0",
15
+ scripts: { dev: "vite", build: "vite build" },
16
+ dependencies: {
17
+ react: "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "react-router-dom": "^6.18.0",
20
+ axios: "^1.6.0",
21
+ },
22
+ devDependencies: {
23
+ vite: "^5.0.0",
24
+ "@vitejs/plugin-react": "^4.2.0",
25
+ tailwindcss: "^3.3.0",
26
+ autoprefixer: "^10.4.16",
27
+ postcss: "^8.4.31",
28
+ },
29
+ },
30
+ null,
31
+ 2
32
+ )
33
+ );
8
34
 
9
- fs.outputFileSync(
10
- path.join(root, "package.json"),
11
- JSON.stringify(
12
- {
13
- name: "frontend-project",
14
- version: "1.0.0",
15
- scripts: { dev: "vite", build: "vite build" },
16
- dependencies: {
17
- react: "^18.2.0",
18
- "react-dom": "^18.2.0",
19
- "react-router-dom": "^6.18.0",
20
- axios: "^1.6.0",
21
- },
22
- devDependencies: {
23
- vite: "^5.0.0",
24
- "@vitejs/plugin-react": "^4.2.0",
25
- tailwindcss: "^3.3.0",
26
- autoprefixer: "^10.4.16",
27
- postcss: "^8.4.31",
28
- },
29
- },
30
- null,
31
- 2
32
- )
33
- );
35
+ fs.outputFileSync(
36
+ path.join(root, "vite.config.js"),
37
+ `import { defineConfig } from 'vite';
38
+ import react from '@vitejs/plugin-react';
39
+
40
+ export default defineConfig({
41
+ plugins: [react()],
42
+ server: {
43
+ port: 5173,
44
+ strictPort: false,
45
+ },
46
+ });
47
+ `
48
+ );
34
49
 
35
- fs.outputFileSync(
36
- path.join(root, "src", "api", "axios.js"),
37
- `import axios from "axios";
50
+ fs.outputFileSync(
51
+ path.join(root, "tailwind.config.js"),
52
+ `export default {
53
+ content: [
54
+ "./index.html",
55
+ "./src/**/*.{js,jsx}",
56
+ ],
57
+ theme: {
58
+ extend: {},
59
+ },
60
+ plugins: [],
61
+ };
62
+ `
63
+ );
64
+
65
+ fs.outputFileSync(
66
+ path.join(root, "postcss.config.js"),
67
+ `export default {
68
+ plugins: {
69
+ tailwindcss: {},
70
+ autoprefixer: {},
71
+ },
72
+ };
73
+ `
74
+ );
75
+
76
+ fs.outputFileSync(
77
+ path.join(root, "index.html"),
78
+ `<!DOCTYPE html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="UTF-8" />
82
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
83
+ <title>${projectName}</title>
84
+ </head>
85
+ <body>
86
+ <div id="root"></div>
87
+ <script type="module" src="/src/main.jsx"><\/script>
88
+ </body>
89
+ </html>
90
+ `
91
+ );
92
+
93
+ fs.outputFileSync(
94
+ path.join(root, ".env.local.example"),
95
+ `VITE_API_URL=http://localhost:5000
96
+ `
97
+ );
98
+
99
+ fs.outputFileSync(
100
+ path.join(root, ".gitignore"),
101
+ `node_modules/
102
+ dist/
103
+ .env
104
+ .env.local
105
+ *.log
106
+ .DS_Store
107
+ .idea/
108
+ .vscode/
109
+ `
110
+ );
111
+
112
+ fs.outputFileSync(
113
+ path.join(root, "src", "api", "axios.js"),
114
+ `import axios from "axios";
38
115
 
39
116
  const API = axios.create({
40
- baseURL: "http://localhost:5000",
117
+ baseURL: import.meta.env.VITE_API_URL || "http://localhost:5000",
41
118
  withCredentials: true,
42
119
  });
43
120
 
44
121
  export default API;
45
122
  `
46
- );
123
+ );
47
124
 
48
- fs.outputFileSync(
49
- path.join(root, "src", "main.jsx"),
50
- `import React from "react";
125
+ fs.outputFileSync(
126
+ path.join(root, "src", "main.jsx"),
127
+ `import React from "react";
51
128
  import ReactDOM from "react-dom/client";
52
129
  import App from "./App";
53
130
  import "./index.css";
@@ -58,46 +135,115 @@ ReactDOM.createRoot(document.getElementById("root")).render(
58
135
  </React.StrictMode>
59
136
  );
60
137
  `
61
- );
138
+ );
62
139
 
63
- fs.outputFileSync(
64
- path.join(root, "src", "index.css"),
65
- `@tailwind base;
140
+ fs.outputFileSync(
141
+ path.join(root, "src", "index.css"),
142
+ `@tailwind base;
66
143
  @tailwind components;
67
144
  @tailwind utilities;
145
+
146
+ body {
147
+ font-family: system-ui, -apple-system, sans-serif;
148
+ }
149
+ `
150
+ );
151
+
152
+ if (needsAuth) {
153
+ fs.outputFileSync(
154
+ path.join(root, "src", "context", "AuthContext.jsx"),
155
+ `import React, { createContext, useState, useEffect } from "react";
156
+ import API from "../api/axios";
157
+
158
+ export const AuthContext = createContext();
159
+
160
+ export function AuthProvider({ children }) {
161
+ const [user, setUser] = useState(null);
162
+ const [loading, setLoading] = useState(true);
163
+
164
+ useEffect(() => {
165
+ const checkAuth = async () => {
166
+ try {
167
+ const res = await API.get("/auth/me");
168
+ setUser(res.data);
169
+ } catch {
170
+ setUser(null);
171
+ } finally {
172
+ setLoading(false);
173
+ }
174
+ };
175
+ checkAuth();
176
+ }, []);
177
+
178
+ return (
179
+ <AuthContext.Provider value={{ user, setUser, loading }}>
180
+ {children}
181
+ </AuthContext.Provider>
182
+ );
183
+ }
184
+ `
185
+ );
186
+
187
+ fs.outputFileSync(
188
+ path.join(root, "src", "components", "PrivateRoute.jsx"),
189
+ `import { Navigate } from "react-router-dom";
190
+ import { useContext } from "react";
191
+ import { AuthContext } from "../context/AuthContext";
192
+
193
+ export default function PrivateRoute({ children }) {
194
+ const { user, loading } = useContext(AuthContext);
195
+
196
+ if (loading) return <div className="p-6">Loading...</div>;
197
+ return user ? children : <Navigate to="/login" />;
198
+ }
68
199
  `
69
200
  );
201
+ }
70
202
 
71
- for (const table of tables) {
72
- const name = toPascal(table.name);
73
- const route = toRoute(table.name);
74
- const fields = table.fields;
75
- const ops = table.operations;
76
-
77
- const stateFields = fields.map((f) => ` ${f}: ""`).join(",\n");
78
- const formReset = fields.map((f) => `${f}: ""`).join(", ");
79
- const editSet = fields.map((f) => `${f}: item.${f}`).join(", ");
80
- const inputs = fields
81
- .map(
82
- (f) => ` <input
203
+ fs.outputFileSync(
204
+ path.join(root, "src", "pages", "Home.jsx"),
205
+ `export default function Home() {
206
+ return (
207
+ <div className="p-6 max-w-2xl">
208
+ <h1 className="text-3xl font-bold mb-4">Welcome</h1>
209
+ <p className="text-gray-600">Select an option from the navigation above to get started.</p>
210
+ </div>
211
+ );
212
+ }
213
+ `
214
+ );
215
+
216
+ for (const table of tables) {
217
+ const name = toPascal(table.name);
218
+ const route = toRoute(table.name);
219
+ const fields = table.fields;
220
+ const ops = table.operations;
221
+
222
+ const stateFields = fields.map((f) => ` ${f}: ""`).join(",\n");
223
+ const formReset = fields.map((f) => `${f}: ""`).join(", ");
224
+ const editSet = fields.map((f) => `${f}: item.${f}`).join(", ");
225
+ const inputs = fields
226
+ .map(
227
+ (f) => ` <input
83
228
  type="text"
84
229
  placeholder="${f}"
85
230
  value={form.${f}}
86
231
  onChange={(e) => setForm({ ...form, ${f}: e.target.value })}
87
232
  className="border p-2 rounded w-full"
233
+ required
88
234
  />`
89
- )
90
- .join("\n");
235
+ )
236
+ .join("\n");
91
237
 
92
- const tableHeaders = ["id", ...fields, "created_at"]
93
- .map((f) => ` <th className="border px-4 py-2">${f}</th>`)
94
- .join("\n");
238
+ const tableHeaders = ["id", ...fields, "created_at"]
239
+ .map((f) => ` <th className="border px-4 py-2">${f}</th>`)
240
+ .join("\n");
95
241
 
96
- const tableRow = ["id", ...fields, "created_at"]
97
- .map((f) => ` <td className="border px-4 py-2">{item.${f}}</td>`)
98
- .join("\n");
242
+ const tableRow = ["id", ...fields, "created_at"]
243
+ .map((f) => ` <td className="border px-4 py-2">{item.${f}}</td>`)
244
+ .join("\n");
99
245
 
100
- let page = `import { useState, useEffect } from "react";
246
+ let page = `import { useState, useEffect } from "react";
101
247
  import API from "../api/axios";
102
248
 
103
249
  export default function ${name}() {
@@ -106,10 +252,18 @@ export default function ${name}() {
106
252
  ${stateFields}
107
253
  });
108
254
  ${ops.includes("update") ? " const [editId, setEditId] = useState(null);" : ""}
255
+ const [loading, setLoading] = useState(false);
256
+ const [error, setError] = useState("");
257
+ const [success, setSuccess] = useState("");
109
258
 
110
259
  const fetchAll = async () => {
111
- const res = await API.get("/${route}");
112
- setItems(res.data);
260
+ try {
261
+ setError("");
262
+ const res = await API.get("/${route}");
263
+ setItems(res.data);
264
+ } catch (err) {
265
+ setError(err.response?.data?.error || "Failed to load data");
266
+ }
113
267
  };
114
268
 
115
269
  useEffect(() => {
@@ -118,66 +272,98 @@ ${ops.includes("update") ? " const [editId, setEditId] = useState(null);" : ""}
118
272
 
119
273
  `;
120
274
 
121
- if (ops.includes("insert")) {
122
- page += ` const handleSubmit = async (e) => {
275
+ if (ops.includes("insert")) {
276
+ page += ` const handleSubmit = async (e) => {
123
277
  e.preventDefault();
124
- ${ops.includes("update") ? ` if (editId) {
125
- await API.put(\`/${route}/\${editId}\`, form);
126
- setEditId(null);
127
- } else {
128
- await API.post("/${route}", form);
129
- }` : ` await API.post("/${route}", form);`}
130
- setForm({ ${formReset} });
131
- fetchAll();
278
+ setLoading(true);
279
+ setError("");
280
+ setSuccess("");
281
+ try {
282
+ ${ops.includes("update") ? ` if (editId) {
283
+ await API.put(\`/${route}/\${editId}\`, form);
284
+ setSuccess("Updated successfully");
285
+ setEditId(null);
286
+ } else {
287
+ await API.post("/${route}", form);
288
+ setSuccess("Created successfully");
289
+ }` : ` await API.post("/${route}", form);
290
+ setSuccess("Created successfully");`}
291
+ setForm({ ${formReset} });
292
+ fetchAll();
293
+ } catch (err) {
294
+ setError(err.response?.data?.error || "Operation failed");
295
+ } finally {
296
+ setLoading(false);
297
+ }
132
298
  };
133
299
 
134
300
  `;
135
- }
301
+ }
136
302
 
137
- if (ops.includes("delete")) {
138
- page += ` const handleDelete = async (id) => {
139
- if (!window.confirm("Are you sure you want to delete this?")) return;
140
- await API.delete(\`/${route}/\${id}\`);
141
- fetchAll();
303
+ if (ops.includes("delete")) {
304
+ page += ` const handleDelete = async (id) => {
305
+ if (!window.confirm("Are you sure?")) return;
306
+ setLoading(true);
307
+ setError("");
308
+ try {
309
+ await API.delete(\`/${route}/\${id}\`);
310
+ setSuccess("Deleted successfully");
311
+ fetchAll();
312
+ } catch (err) {
313
+ setError(err.response?.data?.error || "Delete failed");
314
+ } finally {
315
+ setLoading(false);
316
+ }
142
317
  };
143
318
 
144
319
  `;
145
- }
320
+ }
146
321
 
147
- if (ops.includes("update")) {
148
- page += ` const handleEdit = (item) => {
322
+ if (ops.includes("update")) {
323
+ page += ` const handleEdit = (item) => {
149
324
  setEditId(item.id);
150
325
  setForm({ ${editSet} });
151
326
  };
152
327
 
328
+ const handleCancel = () => {
329
+ setEditId(null);
330
+ setForm({ ${formReset} });
331
+ };
332
+
153
333
  `;
154
- }
334
+ }
155
335
 
156
- page += ` return (
336
+ page += ` return (
157
337
  <div className="p-6">
158
338
  <h1 className="text-2xl font-bold mb-4">${name}</h1>
159
339
 
160
- <form onSubmit={handleSubmit} className="flex flex-col gap-3 mb-6 max-w-md">
340
+ {error && <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded mb-4">{error}</div>}
341
+ {success && <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-2 rounded mb-4">{success}</div>}
342
+
343
+ ${ops.includes("insert") ? ` <form onSubmit={handleSubmit} className="flex flex-col gap-3 mb-6 max-w-md">
161
344
  ${inputs}
162
- <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
163
- ${ops.includes("update") ? 'editId ? "Update" : "Add"' : '"Add"'}
164
- </button>
165
- </form>
345
+ <div className="flex gap-2">
346
+ <button type="submit" disabled={loading} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50">
347
+ {loading ? "Processing..." : (${ops.includes("update") ? 'editId ? "Update" : "Add"' : '"Add"'})}
348
+ </button>
349
+ ${ops.includes("update") ? ' {editId && <button type="button" onClick={handleCancel} className="bg-gray-400 text-white px-4 py-2 rounded hover:bg-gray-500">Cancel</button>}' : ""}
350
+ </div>
351
+ </form>` : ""}
166
352
 
167
353
  <table className="w-full border-collapse text-sm">
168
354
  <thead className="bg-gray-100">
169
355
  <tr>
170
356
  ${tableHeaders}
171
- ${ops.includes("update") || ops.includes("delete") ? ' <th className="border px-4 py-2">Actions</th>' : ""}
357
+ ${(ops.includes("update") || ops.includes("delete")) ? ' <th className="border px-4 py-2">Actions</th>' : ""}
172
358
  </tr>
173
359
  </thead>
174
360
  <tbody>
175
361
  {items.map((item) => (
176
362
  <tr key={item.id} className="hover:bg-gray-50">
177
363
  ${tableRow}
178
- ${ops.includes("update") || ops.includes("delete") ? ` <td className="border px-4 py-2 space-x-2">
364
+ ${(ops.includes("update") || ops.includes("delete")) ? ` <td className="border px-4 py-2 space-x-2">
179
365
  ${ops.includes("update") ? ' <button onClick={() => handleEdit(item)} className="text-blue-600 hover:underline">Edit</button>' : ""}
180
- ${ops.includes("delete") ? ' <button onClick={() => handleDelete(item.id)} className="text-red-600 hover:underline">Delete</button>' : ""}
366
+ ${ops.includes("delete") ? ' <button onClick={() => handleDelete(item.id)} disabled={loading} className="text-red-600 hover:underline disabled:opacity-50">Delete</button>' : ""}
181
367
  </td>` : ""}
182
368
  </tr>
183
369
  ))}
@@ -188,127 +374,168 @@ ${ops.includes("delete") ? ' <button onClick={() => handleDelete(item
188
374
  }
189
375
  `;
190
376
 
191
- fs.outputFileSync(path.join(root, "src", "pages", `${name}.jsx`), page);
192
- console.log(` [✓] pages/${name}.jsx`);
193
- }
377
+ fs.outputFileSync(path.join(root, "src", "pages", `${name}.jsx`), page);
378
+ console.log(` [✓] pages/${name}.jsx`);
379
+ }
194
380
 
195
- if (needsAuth) {
196
- fs.outputFileSync(
197
- path.join(root, "src", "pages", "Login.jsx"),
198
- `import { useState } from "react";
381
+ if (needsAuth) {
382
+ fs.outputFileSync(
383
+ path.join(root, "src", "pages", "Login.jsx"),
384
+ `import { useState, useContext } from "react";
199
385
  import { useNavigate } from "react-router-dom";
386
+ import { AuthContext } from "../context/AuthContext";
200
387
  import API from "../api/axios";
201
388
 
202
389
  export default function Login() {
203
390
  const navigate = useNavigate();
391
+ const { setUser } = useContext(AuthContext);
204
392
  const [form, setForm] = useState({ username: "", password: "" });
205
393
  const [error, setError] = useState("");
394
+ const [loading, setLoading] = useState(false);
395
+ const [isRegistering, setIsRegistering] = useState(false);
206
396
 
207
- const handleLogin = async (e) => {
397
+ const handleSubmit = async (e) => {
208
398
  e.preventDefault();
209
399
  setError("");
400
+ setLoading(true);
210
401
  try {
211
- await API.post("/auth/login", form);
402
+ const endpoint = isRegistering ? "/auth/register" : "/auth/login";
403
+ const res = await API.post(endpoint, form);
404
+ setUser(res.data.user);
212
405
  navigate("/");
213
406
  } catch (err) {
214
- setError(err.response?.data?.message || "Login failed");
407
+ setError(err.response?.data?.message || (isRegistering ? "Registration failed" : "Login failed"));
408
+ } finally {
409
+ setLoading(false);
215
410
  }
216
411
  };
217
412
 
218
413
  return (
219
- <div className="p-6 max-w-md">
220
- <h1 className="text-2xl font-bold mb-4">Login</h1>
221
- <form onSubmit={handleLogin} className="flex flex-col gap-3">
222
- <input
223
- className="border p-2 rounded"
224
- placeholder="Username"
225
- value={form.username}
226
- onChange={(e) => setForm({ ...form, username: e.target.value })}
227
- />
228
- <input
229
- className="border p-2 rounded"
230
- type="password"
231
- placeholder="Password"
232
- value={form.password}
233
- onChange={(e) => setForm({ ...form, password: e.target.value })}
234
- />
235
- {error ? <p className="text-red-600">{error}</p> : null}
236
- <button className="bg-blue-600 text-white px-4 py-2 rounded">Login</button>
237
- </form>
414
+ <div className="flex items-center justify-center min-h-screen bg-gray-100">
415
+ <div className="bg-white p-8 rounded shadow-md max-w-md w-full">
416
+ <h1 className="text-2xl font-bold mb-4">{isRegistering ? "Register" : "Login"}</h1>
417
+ {error && <p className="text-red-600 mb-3">{error}</p>}
418
+ <form onSubmit={handleSubmit} className="flex flex-col gap-3">
419
+ <input
420
+ className="border p-2 rounded"
421
+ placeholder="Username"
422
+ value={form.username}
423
+ onChange={(e) => setForm({ ...form, username: e.target.value })}
424
+ required
425
+ />
426
+ <input
427
+ className="border p-2 rounded"
428
+ type="password"
429
+ placeholder="Password"
430
+ value={form.password}
431
+ onChange={(e) => setForm({ ...form, password: e.target.value })}
432
+ required
433
+ />
434
+ <button type="submit" disabled={loading} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50">
435
+ {loading ? "Processing..." : isRegistering ? "Register" : "Login"}
436
+ </button>
437
+ </form>
438
+ <button
439
+ type="button"
440
+ onClick={() => setIsRegistering(!isRegistering)}
441
+ className="text-blue-600 hover:underline mt-3 w-full text-sm"
442
+ >
443
+ {isRegistering ? "Have an account? Login" : "Need an account? Register"}
444
+ </button>
445
+ </div>
238
446
  </div>
239
447
  );
240
448
  }
241
449
  `
242
- );
243
- }
450
+ );
451
+ }
244
452
 
245
- if (needsReports) {
246
- fs.outputFileSync(
247
- path.join(root, "src", "pages", "Reports.jsx"),
248
- `export default function Reports() {
453
+ if (needsReports) {
454
+ fs.outputFileSync(
455
+ path.join(root, "src", "pages", "Reports.jsx"),
456
+ `export default function Reports() {
249
457
  return (
250
458
  <div className="p-6">
251
459
  <h1 className="text-2xl font-bold mb-4">Reports</h1>
252
- <p>Build your reports dashboard here.</p>
460
+ <p className="text-gray-600">Build your reports dashboard here. Add charts, analytics, and visualizations.</p>
253
461
  </div>
254
462
  );
255
463
  }
256
464
  `
257
- );
258
- }
259
-
260
- const imports = tables
261
- .map((t) => `import ${toPascal(t.name)} from "./pages/${toPascal(t.name)}";`)
262
- .join("\n");
263
-
264
- const routes = tables
265
- .map((t) => ` <Route path="/${toRoute(t.name)}" element={<${toPascal(t.name)} />} />`)
266
- .join("\n");
267
-
268
- const navLinks = tables
269
- .map((t) => ` <Link to="/${toRoute(t.name)}" className="hover:underline">${toPascal(t.name)}</Link>`)
270
- .join("\n");
271
-
272
- const app = `import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
465
+ );
466
+ }
467
+
468
+ const imports = tables
469
+ .map((t) => `import ${toPascal(t.name)} from "./pages/${toPascal(t.name)}";`)
470
+ .join("\n");
471
+
472
+ const routes = tables
473
+ .map((t) => {
474
+ const route = `<Route path="/${toRoute(t.name)}" element={<${toPascal(t.name)} />} />`;
475
+ return needsAuth
476
+ ? ` <Route path="/${toRoute(t.name)}" element={<PrivateRoute><${toPascal(t.name)} /></PrivateRoute>} />`
477
+ : ` ${route}`;
478
+ })
479
+ .join("\n");
480
+
481
+ const navLinks = tables
482
+ .map((t) => ` <Link to="/${toRoute(t.name)}" className="hover:underline">${toPascal(t.name)}</Link>`)
483
+ .join("\n");
484
+
485
+ let app = `import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
273
486
  ${imports}
274
- ${needsAuth ? 'import Login from "./pages/Login";' : ""}
487
+ ${needsAuth ? 'import Login from "./pages/Login";\nimport PrivateRoute from "./components/PrivateRoute";\nimport { AuthContext, AuthProvider } from "./context/AuthContext";\nimport { useContext } from "react";' : ""}
275
488
  ${needsReports ? 'import Reports from "./pages/Reports";' : ""}
489
+ import Home from "./pages/Home";
276
490
  import API from "./api/axios";
277
491
 
278
492
  function Navbar() {
279
493
  const navigate = useNavigate();
494
+ ${needsAuth ? ' const { user } = useContext(AuthContext);' : ""}
280
495
 
281
496
  const logout = async () => {
282
- await API.post("/auth/logout");
283
- navigate("/login");
497
+ try {
498
+ await API.post("/auth/logout");
499
+ navigate("/login");
500
+ window.location.reload();
501
+ } catch (err) {
502
+ console.error("Logout failed:", err);
503
+ }
284
504
  };
285
505
 
286
506
  return (
287
507
  <nav className="bg-blue-700 text-white px-6 py-3 flex gap-6 items-center">
288
- <span className="font-bold text-lg">${projectName}</span>
508
+ <span className="font-bold text-lg"><Link to="/" className="hover:opacity-80">${projectName}</Link></span>
289
509
  ${navLinks}
290
510
  ${needsReports ? ' <Link to="/reports" className="hover:underline">Reports</Link>' : ""}
291
- ${needsAuth ? ' <button onClick={logout} className="ml-auto hover:underline">Logout</button>' : ""}
511
+ ${needsAuth ? ' {user && <button onClick={logout} className="ml-auto hover:underline">Logout ({user.username})</button>}' : ""}
292
512
  </nav>
293
513
  );
294
514
  }
295
515
 
296
- export default function App() {
516
+ function AppRoutes() {
297
517
  return (
298
- <BrowserRouter>
518
+ <>
299
519
  <Navbar />
300
520
  <Routes>
301
521
  ${needsAuth ? ' <Route path="/login" element={<Login />} />' : ""}
522
+ <Route path="/" element={<Home />} />
302
523
  ${routes}
303
- ${needsReports ? ' <Route path="/reports" element={<Reports />} />' : ""}
524
+ ${needsReports ? (needsAuth ? ' <Route path="/reports" element={<PrivateRoute><Reports /></PrivateRoute>} />' : ' <Route path="/reports" element={<Reports />} />') : ""}
304
525
  </Routes>
305
- </BrowserRouter>
526
+ </>
527
+ );
528
+ }
529
+
530
+ export default function App() {
531
+ return (
532
+ ${needsAuth ? ' <AuthProvider>\n <BrowserRouter>\n <AppRoutes />\n </BrowserRouter>\n </AuthProvider>' : ' <BrowserRouter>\n <AppRoutes />\n </BrowserRouter>'}
306
533
  );
307
534
  }
308
535
  `;
309
536
 
310
- fs.outputFileSync(path.join(root, "src", "App.jsx"), app);
311
- console.log(" [✓] App.jsx");
537
+ fs.outputFileSync(path.join(root, "src", "App.jsx"), app);
538
+ console.log(" [✓] App.jsx");
312
539
  }
313
540
 
314
541
  module.exports = { generateFrontend };