tss-stack 1.1.2 → 1.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/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
173
+ ```
174
+
175
+ ### 3. Configure frontend environment (optional)
176
+
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
152
186
  ```
153
187
 
154
- ### 3. Start the backend
188
+ ### 4. Start the backend
155
189
 
156
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.
213
+
214
+ ---
215
+
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
172
250
 
173
251
  ---
174
252
 
175
- ## Requirements
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
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  const ora = require("ora");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tss-stack",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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
  `;
@@ -32,12 +32,89 @@ function generateFrontend(config) {
32
32
  )
33
33
  );
34
34
 
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
+ );
49
+
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
+
35
112
  fs.outputFileSync(
36
113
  path.join(root, "src", "api", "axios.js"),
37
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
 
@@ -65,6 +142,74 @@ ReactDOM.createRoot(document.getElementById("root")).render(
65
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
+ }
199
+ `
200
+ );
201
+ }
202
+
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
+ }
68
213
  `
69
214
  );
70
215
 
@@ -85,6 +230,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
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
235
  )
90
236
  .join("\n");
@@ -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(() => {
@@ -121,14 +275,26 @@ ${ops.includes("update") ? " const [editId, setEditId] = useState(null);" : ""}
121
275
  if (ops.includes("insert")) {
122
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
  `;
@@ -136,9 +302,18 @@ ${ops.includes("update") ? ` if (editId) {
136
302
 
137
303
  if (ops.includes("delete")) {
138
304
  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();
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
  `;
@@ -150,6 +325,11 @@ ${ops.includes("update") ? ` if (editId) {
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
 
@@ -157,27 +337,33 @@ ${ops.includes("update") ? ` if (editId) {
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
  ))}
@@ -195,46 +381,68 @@ ${ops.includes("delete") ? ' <button onClick={() => handleDelete(item
195
381
  if (needsAuth) {
196
382
  fs.outputFileSync(
197
383
  path.join(root, "src", "pages", "Login.jsx"),
198
- `import { useState } from "react";
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
  }
@@ -249,7 +457,7 @@ export default function Login() {
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
  }
@@ -262,47 +470,66 @@ export default function Login() {
262
470
  .join("\n");
263
471
 
264
472
  const routes = tables
265
- .map((t) => ` <Route path="/${toRoute(t.name)}" element={<${toPascal(t.name)} />} />`)
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
+ })
266
479
  .join("\n");
267
480
 
268
481
  const navLinks = tables
269
482
  .map((t) => ` <Link to="/${toRoute(t.name)}" className="hover:underline">${toPascal(t.name)}</Link>`)
270
483
  .join("\n");
271
484
 
272
- const app = `import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
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
  `;