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 +104 -15
- package/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/generators/backend.js +41 -13
- package/src/generators/database.js +2 -1
- package/src/generators/frontend.js +284 -57
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
|
-
|
|
119
|
-
|
|
120
|
-
- **`
|
|
121
|
-
- **`
|
|
122
|
-
- **`
|
|
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/
|
|
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=
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
206
|
+
Frontend runs on `http://localhost:5173`
|
|
168
207
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
##
|
|
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
package/package.json
CHANGED
|
@@ -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=
|
|
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.
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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: "
|
|
166
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
const user = results[0];
|
|
148
|
-
const
|
|
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: "
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
397
|
+
const handleSubmit = async (e) => {
|
|
208
398
|
e.preventDefault();
|
|
209
399
|
setError("");
|
|
400
|
+
setLoading(true);
|
|
210
401
|
try {
|
|
211
|
-
|
|
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="
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
516
|
+
function AppRoutes() {
|
|
297
517
|
return (
|
|
298
|
-
|
|
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
|
-
|
|
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
|
`;
|